Octocrawl

Web scraping para agentes de IA: páginas públicas como Markdown, tabelas e JSON, cada uma com um Registro de Evidência (URL final, status HTTP, decisão do robots.txt, hashes). Páginas bloqueadas ou vazias retornam com o motivo. Hospedado (sem conta, 20 páginas/dia) ou local via npx.

Servidor MCP hospedado

npx add-mcp 'https://mcp.octocrawl.dev/mcp'

Instala no Claude Code, Codex, Cursor e outros

Documentação

Octocrawl — Extração de Contexto Web para LLM

Um sistema de extração web transparente e verificável, construído para fluxos de trabalho RAG e Agent.

Experimente sem instalar nada em octocrawl.dev, leia a documentação, ou conecte seu agente ao Octocrawl hospedado em uma linha, sem necessidade de chave para começar (como, para Claude Code, Cursor, OpenCode e Codex):

claude mcp add --transport http octocrawl https://mcp.octocrawl.dev/mcp

Por Que Isso Existe

A maioria dos crawlers relata "sucesso" quando retornam páginas vazias, telas de desafio ou o conteúdo errado. O Octocrawl torna a falha visível e corrigível:

Antes (crawler típico):

✓ Fetched example.com/article
  Status: 200 OK
  Content: 953 bytes

Depois (Octocrawl):

✗ Fetched example.com/article
  Status: blocked (cloudflare_challenge)
  Lane: http → escalated to browser_local
   Evidence: artifacts=[] (no screenshot or DOM snapshot was produced)
  Cost: 847 tokens, 2.3s, $0.0042
  Fix: needs user login or proxy (tier 1b/2)

O Que é Diferente

  1. Falha é um resultado de primeira classe — empty_verified, blocked, failed com motivos, não vazios silenciosos; uma página que respondeu com status de erro mantém seu httpStatus e Markdown como evidência, nunca como sucesso, e o mesmo ocorre com uma página na qual o Octocrawl não encontra conteúdo principal (failed/empty_unverified com o Markdown da página inteira), uma região principal que a página oculta com hidden e uma região de artigo que apenas nomeia a página incluída (cabeçalhos com menos de 100 caracteres e quase nada além deles, nenhuma imagem que o Markdown carregue, links de salto na página como "Pular para Filtros" colocados de lado)
  2. Cinco verificações de falso sucesso — texto de desafio, conteúdo de página errada, fatos ausentes, truncamento, rendimento abaixo do mínimo
  3. Escada de execução — HTTP → navegador → autenticação de usuário → proxy, com roteamento automático, rastreamento por tentativa e contabilidade de custos em nível de tarefa
  4. Benchmark de verdade fundamental — uma suíte de 56 casos (soft 404s, páginas de desafio, SPAs, timeouts, zip bombs, tabelas) com taxas verificadas de falso sucesso
  5. Evidência honesta — artifacts: [] é uma lista explícita de artefatos vazios, não uma promessa de que toda página com falha tem uma captura de tela ou snapshot do DOM; bytesWire: null do navegador significa que os bytes de rede não foram medidos
  6. Um Registro de Evidência — todo resultado de raspagem, item de lote e página de rastreamento carrega evidenceRecord (URL final, cadeia de redirecionamentos, tempo de busca, status e motivo, via, decisão do robots.txt, hashes brutos e de saída, evidência de campo), declarado da mesma forma em todas as vias e descrito por um JSON Schema versionado; veja a referência

Início Rápido

A pré-visualização web de página única, sem instalação, roda em octocrawl.dev (cinco pré-visualizações por dia; veja Pré-visualização pública para saber como é implantada). Além de Markdown, ela retorna os links e metadados de uma página, e até 20 campos lidos da página sem um modelo. O mesmo site serve a documentação: etapas de conexão MCP para quatro clientes, quatro guias de tarefas e limites. As páginas são geradas a partir de Markdown neste repositório durante a compilação do site público; npm run public:preview:local as serve em http://127.0.0.1:8798/docs/.

Não instale nada primeiro: com Node.js 22.13+ ou 24+,

npx octocrawl scrape https://example.com --markdown
pip install octocrawl-client      # the Python client of a running API (npx octocrawl serve)
npm install @octocrawl/sdk        # the TypeScript client; @octocrawl/mcp is the MCP server

Os pacotes são publicados a partir deste repositório (0.3.0 em 2026-10-05); o fluxo de trabalho Install check executa a linha npx a partir de um cache vazio no macOS, Windows e Linux toda semana. Para trabalhar no código, use Node.js 22.13+ ou 24+ e npm (o mecanismo de texto PDF, pdf.js, precisa de 22.13 ou posterior) e compile a partir deste checkout. Para o fluxo de trabalho completo Monitor → resultado → evento HTTPS → reinicialização, siga o guia de integração e a lista de verificação de aceitação independente para desenvolvedores.

git clone https://github.com/77777R7/w2l.git
cd w2l
npm ci
npx playwright install chromium
npm run typecheck
npm test
npm run scrape -- https://example.com --markdown
npm run crawl -- https://example.com --max-pages 20

octocrawl (o pacote @octocrawl/cli, também publicado como octocrawl; npm run w2l -- <command> em um checkout) executa o mecanismo da API em seu próprio processo, então toda opção da API REST funciona na linha de comando. Cada opção é uma flag sob seu nome kebab-case: maxAge é --max-age, onlyMainContent: false é --no-only-main-content, includeTags aceita a,b e pode se repetir, formats aceita markdown,tables ou um array JSON para uma entrada com opções, parsers aceita pdf, none ou JSON, headers aceita JSON ou --header name=value (repetível), e as URLs são argumentos. A solicitação é verificada pelo próprio parser da API REST, então um valor que a API recusa é recusado aqui com a mesma mensagem (código de saída 2).

  • octocrawl scrape <url> imprime a resposta da raspagem como JSON (compacto, como o MCP a recebe; --debug para a versão completa), ou apenas o Markdown com --markdown.
  • octocrawl batch <url>... (ou --urls-file <file>, uma URL por linha) e octocrawl crawl <url> executam até o fim e imprimem { report, items }. Ctrl-C deixa o trabalho pausado: octocrawl crawl --resume <taskId> continua um rastreamento (um rastreamento registrado como pendente ou em execução é recusado, pois outro processo pode estar executando-o), e um lote é retomado quando a API inicia na mesma raiz de tarefa. --webhook não é oferecido, pois um comando não executa nenhum worker de entrega: envie o trabalho para octocrawl serve em vez disso.
  • octocrawl map <url> imprime o mapa.
  • --out <dir> escreve results.jsonl (um resultado por linha), results.csv (uma linha por página com sua evidência, páginas com falha incluídas: as colunas do to_pandas() do cliente Python sem o Markdown, depois markdown_file), report.json para um trabalho, e por página <n>-<host-path>.md com seu Markdown e <n>-<host-path>.table-<i>.csv para cada tabela do formato tables.
  • octocrawl serve executa a API local, como npm run api faz, com as mesmas flags (--port, --host, --hosted, --token, ...).
  • octocrawl login import <site> salva seu login em um site a partir do Chrome que você já usa, então --mode authed lê suas páginas autenticado como você; octocrawl login list e octocrawl login remove <site> mostram e esquecem logins salvos sem imprimir um cookie. Veja Seu próprio login (modo authed).

Códigos de saída: 0 para uma página lida como conteúdo ou um trabalho concluído, 1 para qualquer outra coisa que o Octocrawl respondeu, 2 para uma linha de comando recusada, 130 quando interrompido. A raiz de tarefa, onde tarefas, arquivos salvos e o cache de páginas vivem, é --task-root, senão W2L_TASK_ROOT, senão .w2l/cli, exceto o .w2l/api da API; nunca aponte um comando para a raiz de tarefa de um servidor de API em execução, que poderia executar o mesmo trabalho duas vezes. Um comando nunca retoma os trabalhos anteriores da raiz de tarefa, como a API faz quando inicia. A ferramenta de escada anterior em processo é w2l-ladder (e w2l-fetch) em @w2l/bench.

Seu próprio login (modo authed)

O modo authed lê uma página com o login que você salvou para seu site, no próprio navegador do Octocrawl. Para salvar um, faça login no site no Google Chrome (144 ou posterior), no perfil padrão e em uma janela normal; abra chrome://inspect/#remote-debugging e ative "Allow remote debugging for this browser instance" uma vez; então execute octocrawl login import example.com (um domínio ou uma URL de página). O Chrome pergunta "Allow remote debugging?" para cada conexão: clique em Allow. O Octocrawl conecta uma vez, lê os cookies desse site (os próprios, os de um domínio pai e os de subdomínios quando o site define cookies no próprio nome) e o localStorage das abas do site que você tem abertas, salva-os e desconecta; ele nunca lê os arquivos do Chrome no disco e lê o armazenamento de uma aba sem carregar nada ou executar script nela. Os logins são mantidos em W2L_SESSIONS_FILE, senão ~/.w2l/sessions.json, legíveis apenas por você, e a linha de comando, a API local e o serviço MCP local leem o mesmo arquivo. Os registros carregam o SHA-256 do login, a contagem de cookies e as origens do localStorage e a contagem de itens, nunca um valor. A mesma importação funciona sem a linha de comando em um servidor rodando em sua máquina: POST /v1/logins/import com { "site": "example.com" } (e approveTimeoutMs, quanto tempo esperar pelo seu Allow, 10 s a 10 min, padrão 2 min) responde { domain, savedAt, cookieCount, localStorage, localStorageRead, sessionSha256 } assim que você clicar em Allow (localStorage: { origins, itemCount } salvo, ou null; localStorageRead false quando nenhuma aba do site estava aberta, então nenhuma foi lida; localStorageUnread, as origens das abas abertas cujo armazenamento o Chrome não forneceu, uma aba travada ou descartada, que um recarregamento traz de volta). GET /v1/logins lista os logins salvos e DELETE /v1/logins/:site esquece um. Os importLogin, listLogins e removeLogin do SDK, e as ferramentas MCP locais import_login, list_logins e remove_login, chamam essas rotas. Um servidor MCP local oferece todas as três. import_login permite que um agente escolha qual login de site salvar, então sua descrição diz ao agente para perguntar a você primeiro, e o Allow do Chrome ainda é seu para clicar. Nenhuma rota responde um cookie. Um servidor que não está em loopback, o serviço hospedado e um servidor sem seu Chrome recusam uma importação com 409.

  • Um login para example.com cobre www.example.com e outros subdomínios. Quando um se aplica, o degrau autenticado vai primeiro, antes dos degraus públicos, que tomariam uma página deslogada como resposta; se o site recusar o login (login_wall, por exemplo, quando expirou), a execução termina aí em vez de retornar a página deslogada. Uma recusa é uma página login_wall, um redirecionamento para a página de login do site ou uma página que pede um login no lugar: um cabeçalho ou linha simples entre os primeiros oito que começa com a solicitação, como "Log in to view your wishlists", "Please sign in", "You must be logged in" ou "Login required". O link Sign in de um cabeçalho, uma linha de tabela, um item de lista, uma linha com um link, uma linha onde as palavras vêm depois de outras ("Step 2: Sign in to continue") e prosa mais abaixo são o conteúdo da página, não uma recusa; também é um cabeçalho que o título da página nomeia (um problema ou pergunta cujo assunto começa com essas palavras, "Please log in again #1411"). A regra corresponde apenas ao inglês. ladder_session_rejected nomeia quais: redirectedTo ou signInPrompt. Importe o site novamente para substituir um login expirado; um servidor em execução usa o novo imediatamente.
  • O modo authed funciona em raspagem e lote, não em rastreamento: um rastreamento segue todos os links, e um link de saída encerraria sua sessão também no Chrome, já que os cookies salvos são dessa sessão. Envie as páginas como um lote.
  • Um site que mantém seu login em localStorage (um token que seu script lê) precisa de uma aba dele aberta quando você importa: o Chrome lê o armazenamento de uma origem apenas através de uma página que o mostra, então com nenhuma aberta a importação salva apenas os cookies e diz localStorageRead: false. Uma aba cujo armazenamento o Chrome não fornece (travou, foi descartada ou fechada enquanto isso) é salva sem ele e nomeada em localStorageUnread, com a solicitação ao Chrome que falhou e a resposta do Chrome em localStorageUnreadReasons. Apenas abas no perfil de onde os cookies vêm são lidas, nunca as de uma janela anônima, e apenas a própria origem de cada aba, não um frame de outra origem dentro dela; sessionStorage e IndexedDB não são salvos. A depuração remota do Chrome alcança apenas o perfil padrão.
  • Um site pode vincular seu login a mais do que os cookies (uma sessão no servidor, o navegador, o endereço de rede); o Octocrawl não imita seu navegador, então tal site responde como se você estivesse deslogado (veja a execução I1).

Acesso aprimorado (uma concessão de acesso)

Para pessoas que não querem os interruptores técnicos, uma opção escolhe como as páginas são alcançadas: "access": "standard", "enhanced" ou "my-browser" em POST /v1/scrape e POST /v1/batches (standard ou enhanced em POST /v1/crawl), as ferramentas MCP scrape, batch_scrape e crawl, e --access na linha de comando.

  • standard: a busca própria do Octocrawl e seu navegador local. Nenhum degrau que custa um terceiro é executado; a auditoria de rota diz quais foram descartados (ladder_channels_filtered, motivo access standard).
  • enhanced: também o que a concessão de acesso do servidor de nível aprimorado aprova: os provedores pagos abaixo, em qualquer modo, incluindo o modo padrão. O orçamento de execução da concessão limita um lote ou um rastreamento; uma raspagem única chama cada provedor que o servidor nomeia no máximo uma vez. Um servidor sem tal concessão recusa por nome (unsupported_parameter).
  • my-browser: seu próprio Chrome, o mesmo que "lane": "my-browser" (abaixo). Um rastreamento não o utiliza.

Sem access, uma solicitação é executada conforme o servidor está configurado. Um lote ou rastreamento mantém sua escolha, então uma execução retomada faz a mesma. As opções por rota abaixo permanecem para desenvolvedores.

Por padrão, um servidor não usa nenhum dos recursos que o ADR 0005 coloca atrás de uma concessão: nenhum navegador de provedor, nenhuma resolução de desafio, nenhum stealth de provedor. Um operador que os deseja inicia o servidor com uma concessão, um arquivo JSON que o servidor verifica na inicialização; uma concessão com qualquer problema interrompe a inicialização com todos os problemas listados.

octocrawl serve --access-grant grant.json
{
  "tier": "enhanced",
  "capabilities": ["vendor_remote_browser", "vendor_captcha_solving"],
  "budget": { "perRunUsd": 5, "perRequestUsd": 0.5 },
  "tariffs": { "browserbase": { "perHourUsd": 0.12, "maxSessionMs": 120000, "minBilledMs": 60000, "billingIncrementMs": 60000 } },
  "attestation": { "principal": "you@example.com", "at": "2026-10-05T12:00:00Z", "statement": "I accept the provider's terms and the cost of these routes." }
}
  • W2L_ACCESS_GRANT aceita o mesmo, como caminho de arquivo ou o próprio JSON.
  • Um recurso que o ADR 0005 recusa (rotação de identidade, aplicação de patch no seu próprio Chrome e outros que lista) ou adia (um mecanismo de navegador próprio, Camoufox e outros) é um erro, não ignorado; um adiado nomeia a linha do ROADMAP que diz quando reinicia.
  • Uma concessão standard ou my_browser pode nomear apenas compatible_transport e egress_sessions. Um recurso que pode custar dinheiro precisa de um perRunUsd positivo, e um enhanced precisa de uma atestação.
  • Degraus de provedor (W2L_VENDORS com sua chave, no modo research ou authed) são construídos apenas quando a concessão nomeia vendor_remote_browser; a resolução de desafios e o stealth do provedor seguem vendor_captcha_solving e vendor_stealth.
  • A escada continua para seu próximo degrau (o navegador, depois um provedor) para um bloqueio ou verificação que reconhece, e também para um 403 ou 405 respondido sem um (a defesa de bot de um site frequentemente responde assim), para uma página renderizada por um navegador sem conteúdo principal que pudesse verificar, e para a conexão recusada do degrau HTTP (seu próximo degrau é o navegador, cuja própria falha de rede encerra a execução); o rastreamento diz o porquê (ladder_step com escalate). Quando os degraus mais fortes então falham sem uma página (um erro de rede, um erro do próprio provedor, o prazo), a página que ele ultrapassou é a resposta (ladder_evidence_kept). Ele para em um tempo limite (um site lento ou morto seguraria a raspagem também pela espera do navegador), em um limite de taxa (429, cujo Retry-After um lote ou rastreamento honra), em um status de erro que é a própria resposta da página (404, 410, 5xx), no degrau do seu login salvo (os degraus após ele não carregam o login), e após um degrau anterior encontrar conteúdo, que permanece a resposta.
  • Um provedor é chamado apenas em um teto de preço conhecido (item 4 do PA do ROADMAP). tariffs nomeia, por provedor, os preços que você aceitou de sua página de preços (perCallUsd, perHourUsd) e o que limita o tempo de uma chamada: um preço por hora precisa de maxSessionMs, a sessão mais longa que uma chamada pode manter; minBilledMs é o menor tempo que o provedor cobra por uma sessão, e billingIncrementMs o passo em que cobra (um minuto, arredondado para cima). O teto de uma chamada é perCallUsd mais perHourUsd para sua sessão mais longa, pelo menos minBilledMs e pelo menos o menor tempo limite que o provedor aceita (60 s para Browserbase, 15 s para Steel), arredondado para cima até o passo. Sob uma tarifa, cada chamada abre uma conexão e uma sessão próprias, nunca compartilhadas com outra chamada, encerra-as em maxSessionMs e as libera; a sessão também é criada com o próprio tempo limite do provedor e seus proxies desativados, então termina no lado do provedor se a liberação falhar, e não cobra largura de banda. Um preço por GB é recusado: os bytes que o navegador de um provedor recebe em cada alvo que abre não podem ser contados daqui, então um custo de largura de banda não tem teto. Um provedor sem tarifa não é chamado (ladder_channel_skipped, no price ceiling).
  • O gasto é reservado antes de cada chamada e liquidado após ela, em um livro-razão por tarefa que cada página, nova tentativa e provedor de um lote ou rastreamento compartilham, e que uma execução retomada ou anexada abre com o que a tarefa já foi cobrada: uma chamada é feita apenas quando seu teto cabe no que perRunUsd (e o próprio limite da tarefa) deixou, então páginas concorrentes não podem ultrapassá-lo, e é liquidado no preço que o provedor relatou ou, quando não relatou nenhum (Browserbase e Steel não declaram preço por solicitação), em seu teto, nunca abaixo do custo; uma chamada que lançou erro ou que o prazo cortou é cobrada em seu teto. perRequestUsd limita as chamadas de uma página dentro da execução, e as de uma raspagem única (senão perRunUsd faz). Uma chamada cujo teto não cabe é pulada e o rastreamento diz isso (budget); uma tarefa cujo limite se esgotou para (budget_exceeded com cost). A resposta mantém usage.externalCostUsd para o custo exato (nulo quando desconhecido) e adiciona externalCostChargedUsd, o que o livro-razão cobrou de todas as chamadas da execução, com um evento de rastreamento spend_settled por chamada. Um provedor chamado sem orçamento algum é reservado e liquidado em um livro-razão sem limite da própria execução, então suas chamadas também ficam registradas. O Registro de Evidências de cada página os lista em access.paidCalls, em ordem: o provedor (provider), o degrau, os recursos do ADR 0005 com os quais sua sessão foi criada (capabilities), o teto reservado (ceilingUsd), o que o livro-razão cobrou (chargedUsd), o preço que o provedor declarou (reportedCostUsd, nulo quando não declarou nenhum), e outcome e reason, o que o Octocrawl fez da página que a chamada retornou por suas próprias verificações (uma página de bloqueio, uma leitura vazia ou não verificada, uma identidade que não enviou), nunca a palavra do provedor de que teve sucesso, nulo quando a chamada não retornou página; answer marca a chamada cuja página é a do registro. access.grant nomeia a concessão sob a qual foram feitas pelo SHA-256 de seu texto (shasum -a 256 grant.json dá o mesmo), seu nível e seu tempo de atestação, nunca o principal ou a declaração da atestação. Uma página lida novamente em outro egresso mantém as chamadas da leitura que abandonou, uma página lida no seu Chrome após uma verificação mantém as da execução que a verificação parou, e uma página de lote ou rastreamento cuja execução lançou erro após uma chamada paga a mantém (nenhuma dessas é seu answer); uma página do cache lista as da busca que reutiliza. Ambos são null quando nenhum provedor foi chamado. O custo de um modelo para extração JSON é relatado separadamente (modelUsage) e não contado. Uma concessão que nomeia scope.hosts é recusada, já que nada limita as rotas a esses hosts ainda.
  • W2L_BROWSER_ENGINE=patchright executa o degrau de navegador público no Patchright, um fork mantido do Playwright, quando a concessão nomeia enhanced_browser; um servidor hospedado o recusa, e o degrau de login salvo e uma sessão gerenciada mantêm o Playwright padrão. O Patchright não é instalado com o Octocrawl: instale os dois juntos (npm install octocrawl patchright, ou npx -p octocrawl -p patchright octocrawl ...), depois npx patchright install chromium. Uma etapa executeJavascript ainda é executada no próprio mundo JavaScript da página nele. Uma página buscada nele diz isso em seu rastreamento (browser_engine). Permanece um experimento: em 2026-10-06, em uma saída, alcançou não mais páginas que o Playwright padrão e não perdeu nenhuma (registro), então o Octocrawl não o ativa por padrão.
  • W2L_COMPAT_HOSTS=example.com,shop.example envia as páginas desses hosts, e seus subdomínios, por um transporte HTTP compatível com navegador (impit) quando a concessão nomeia compatible_transport: o degrau http torna-se http_compat, no modo padrão em um servidor local; um servidor hospedado o recusa. A solicitação carrega os próprios cabeçalhos e handshake TLS do Chrome, então a página registra essa identidade do Chrome (identity_sent) e o transporte (transport). Uma solicitação com headers ou mobile personalizados mantém o degrau http, já que o transporte não pode enviá-los sem mudar o conjunto de cabeçalhos do Chrome, e o mesmo faz a página inicial de um mapa, lida sob a identidade que o mapa relata. O impit decodifica corpos comprimidos por conta própria, então tal página relata seu tamanho de fio como desconhecido. Sem W2L_COMPAT_HOSTS, uma concessão que nomeia compatible_transport o usa para os hosts que sua aceitação mostrou ajudar: cinco hosts (research/access/benefit-hosts.v1.json) cuja tarefa bloqueada verificou em ambas as janelas G1 sem regressão (research/access/runs/2026-10-07-g1-acceptance-5afe577.md). Não é para todo host: em todo o conjunto de tarefas também transformou recusas em respostas cujos dados estavam errados. W2L_COMPAT_HOSTS=none o desliga; nomear hosts substitui a lista.
  • egress_sessions dá a cada lote e rastreamento (não uma raspagem única, nem o modo authed, que tem seu login salvo) uma sessão de cookies: os cookies que suas páginas definem são enviados novamente ao site em suas páginas posteriores, pelo degrau HTTP, pelo transporte compatível e pelo navegador igualmente, então uma página cuja verificação o navegador limpou deixa a próxima página da tarefa daquele site ir por HTTP. Os cookies são correspondidos a domínio e caminho como um navegador faz, mantidos no diretório da tarefa (cookie-session.<route>.json, um por rota de egresso, legível apenas por você) para que um lote ou rastreamento retomado após um reinício continue com eles, excluídos quando a tarefa termina, e nunca registrados: o rastreamento de uma página nomeia o id aleatório da sessão e contagens (session_cookies). Uma página lida com uma sessão não é armazenada em cache.
  • W2L_EGRESS_PROXIES=http://user:pass@proxy-a:8080,http://proxy-b:3128 (com egress_sessions; um servidor hospedado o recusa) envia cada busca através de seus próprios proxies. Um lote ou rastreamento mantém um para sua execução, e sua sessão de cookies pertence a ele; move-se para o próximo proxy saudável apenas quando o próprio proxy falha: após uma página não obter resposta HTTP de nenhum degrau, o Octocrawl pede ao proxy um túnel para um nome que não existe, e segue em frente apenas se o proxy não responder ou recusar suas credenciais (407), no máximo duas vezes por execução, com uma nova sessão de cookies, e a página é lida novamente lá (egress_switched em seu rastreamento). Nunca se move por causa do que um site fez: um bloqueio, um desafio, um 429 ou uma conexão que o site reiniciou permanece em seu proxy, já que mover para outro endereço para contornar um é rotação de identidade, que o Octocrawl não faz. Um proxy que falhou é posto de lado por 10 minutos; uma raspagem pega o próximo saudável. Uma tarefa retomada em outra rota (o pool ou o proxy mudou) inicia uma nova sessão de cookies; o modo authed nunca usa o pool. Registros nomeiam um proxy por host:port (proxy), nunca suas credenciais. Com W2L_EGRESS_ECHO_URL definido para um serviço que responde com o endereço do chamador (https://ipinfo.io/json, https://api.ipify.org, https://httpbin.org/ip), cada proxy é perguntado através de si mesmo uma vez, novamente após falhar ou após 10 minutos, e cada página lida através desse proxy registra de onde saiu como access.egress.exit ({ ip, country, observedAt }, o país quando o serviço dá um); sem ele, ou quando o eco não respondeu, exit é null. A solicitação de eco vai ao serviço que você nomeia, através do seu proxy, em uma conexão própria: um gateway que dá a cada conexão ou sessão uma nova saída pode ter enviado a página de outro endereço, então exit é onde esse proxy foi visto saindo, não prova do endereço da própria página. Uma página do cache mantém a saída que sua leitura original registrou, e uma página nunca espera pelo eco além de seu timeout (a saída é então null).
  • vendor_unlock_html e third_party_captcha_solver podem ser concedidos, mas as rotas que os usam ainda não são construídas (itens 4 e 6 do PA do ROADMAP).
  • Os comandos octocrawl leem W2L_ACCESS_GRANT também, pois seu mecanismo roda em seu próprio processo.
  • A concessão faz parte da chave de cache da página, então uma página buscada sob uma concessão não é reutilizada sob outra.

Uma verificação que você mesmo passa (handoff)

Por padrão, o Octocrawl não resolve captchas ou desafios e não se disfarça (veja a concessão de acesso acima para o que uma concessão muda). Quando uma página de um lote para em um deles (blocked com captcha, cloudflare_challenge, bot_detected_generic ou login_wall), o Octocrawl na sua própria máquina pode entregá-la a você no Chrome que você já usa: octocrawl batch <urls> --handoff, POST /v1/batches/:id/handoff em um servidor local (octocrawl serve em loopback), ou a ferramenta MCP local hand_off_batch. A depuração remota deve estar ativada, como para octocrawl login import, e o Chrome pergunta "Permitir depuração remota?" uma vez por transferência. Enquanto a depuração remota estiver ativada, toda página que o Chrome abre vê navigator.webdriver como true, esteja o Octocrawl conectado ou não (visto em 2026-10-07 no Chrome 153 com a opção chrome://inspect, e no Chrome 154 iniciado com --remote-debugging-port): um site que procura por isso, como verificações de bot podem fazer, considera seu Chrome como automatizado, e o Chrome mostra "O Chrome está sendo controlado por software de teste automatizado". O Octocrawl não esconde isso, pois nunca altera seu Chrome. Desative a depuração remota em chrome://inspect/#remote-debugging quando terminar.

Para uma página, peça-a na solicitação: octocrawl scrape <url> --handoff, ou "handoff": true (ou { "waitMs": 60000 }) em POST /v1/scrape, o scrape do SDK e a ferramenta MCP scrape. Quando a própria busca do Octocrawl para em tal verificação, a página abre no seu Chrome da mesma forma, e o scrape responde com a página que você consegue acessar. Essa resposta é registrada como abaixo, e sua auditoria de roteamento é a da execução interrompida. Uma página que não é lida responde como interrompida, com um aviso handoff_not_through que diz o porquê. O timeout do scrape limita a busca do Octocrawl, não o seu tempo; o SDK espera pela resposta enquanto a transferência levar. Sem a opção, uma página interrompida em um servidor que oferece a transferência carrega handoff: { reason, liveViewUrl: null, rationale }, dizendo como pedi-la. Um servidor que não oferece a transferência recusa a opção (unsupported_parameter), assim como faz ao lado de actions ou uma captura de tela.

  • Enquanto o lote espera. Ele executa até o fim como de costume. O status do lote conta os itens interrompidos em waitingForPerson. Cada um desses itens carrega handoff: { reason, liveViewUrl: null, rationale }, onde reason é captcha_required, bot_gate ou login_required. Um limite de taxa ou um bloqueio de região não é transferido.

  • O que acontece quando você transfere. Cada página interrompida abre em uma nova aba do seu Chrome, uma de cada vez. Você passa pela verificação lá, como faria por conta própria. O Octocrawl então lê a página e fecha a aba. Uma página conta como acessada quando, em três leituras com um segundo de intervalo, todas estas condições valem:

    • ela carregou e seu documento respondeu 2xx;
    • o portão do Octocrawl, dado o status e os cabeçalhos do próprio documento, não encontra verificação nele;
    • está no site solicitado (aquele host, um subdomínio ou um domínio pai) e não em um caminho de login;
    • você não está em uma etapa sua: nenhum campo de senha ou código de uso único visível na página, e nenhum campo de formulário cujo valor você está alterando (uma caixa de busca que apenas tem o foco não conta).

    O endereço da página é o que o Chrome mostra, não o que o script da página diz. Uma página que se recarrega (um desafio que executa um script e depois recarrega) é aguardada, não considerada uma aba fechada. Um caminho que termina em outro lugar do site (um login que cai na página inicial) é seguido pelo Octocrawl levando a aba de volta à página solicitada, no máximo duas vezes; uma aba ainda em outro lugar após isso não é lida. A página solicitada é a própria URL, ou onde a URL leva quando o Octocrawl leva a aba de volta a ela, ou essa página com seu endereço reescrito por seu próprio script uma vez que chegou, sem novo documento e antes de você clicar ou digitar nela (o Indeed descarta seu token de paginação e nomeia o trabalho que mostra): o mesmo caminho, o mesmo valor para cada parâmetro que ambos os endereços nomeiam, e nenhum número descartado (um start=10 ou page=2 que desaparece pode significar que o site caiu para sua primeira página). Um endereço que seu próprio clique ou tecla moveu (Próximo, uma ordenação) não é a página solicitada, nem é outra página de uma lista; a aba é levada de volta.

    O Octocrawl lê uma página no seu Chrome somente depois que você agiu na aba dela: um clique ou uma tecla que o próprio Chrome conta como do usuário (a ativação de usuário do documento, lida em um mundo próprio do Octocrawl que o script da página não pode alcançar, ou uma navegação que o Chrome marca como feita com um gesto de usuário). Nada que a página faz por conta própria conta: nem recarregar, redirecionar, uma verificação que passa sozinha, ou um script preenchendo um campo. Uma página que não mostra verificação no seu Chrome (você já está conectado lá, por exemplo) é lida somente quando você clica nela; octocrawl batch --handoff avisa você, e até que você faça isso, o item mantém seu resultado interrompido. Um clique conta apenas na aba que o Octocrawl abriu: quando essa aba fica fora de vista por 3 s (outra aba ou janela na frente dela), octocrawl scrape --handoff e octocrawl batch --handoff avisam você para alternar para ela. Então, um único Permitir nunca deixa um chamador ler os sites onde você está conectado sem você; a faixa meu-navegador abaixo lê sem clique apenas nos sites que você permitiu na página própria do Octocrawl no seu Chrome. Para ler páginas com seu login e sem transferência, importe-o para o site (octocrawl login import) e execute o lote no modo authed.

  • O que substitui o resultado interrompido. Apenas uma leitura que é a página (success ou partial) substitui o resultado interrompido do item, nos formatos que o lote pediu, sob o id próprio do item; uma leitura que ainda mostra uma verificação, ou é um erro ou vazia, deixa o resultado interrompido de pé. A página é registrada como o que é: faixa browser_local_authed, modo authed, compliance: null (o Octocrawl não enviou nada, então não assina nada), usage.requestCount: 0, o User-Agent desconhecido (identity_unobserved: seu navegador enviou a solicitação), o status do documento e Content-Type como seu navegador os recebeu, e a decisão de robots.txt da busca interrompida. O rastro registra o resultado interrompido em handoff_from e a leitura em user_browser_read, com como você agiu (act: user_activation ou gesture_navigation) e a verificação que o Octocrawl viu (sawGate); a auditoria de roteamento da execução interrompida é descartada com ele.

  • Quando uma página não é lida. Você pode não conseguir acessar a tempo (waitMs, 10 s a 30 min, padrão 10 min por página; uma página deixada em outro site, ou uma em que você não clicou, é abandonada quando esse tempo termina), fechar a aba ou sair do Chrome; ou o chamador pode ir embora (a conexão da solicitação fecha, Ctrl-C no CLI), ou o Octocrawl pode desligar; cada um termina a espera e fecha a aba. Uma página que o Chrome se recusa a abrir ou responder não é lida, e as outras ainda são transferidas. Esse item mantém seu resultado interrompido, e a resposta diz o porquê: { id, handedOff, through, notThrough, items: [{ id, url, through, status, reason? }] }.

  • Uma lista que parou em uma verificação. Para um lote cujo único passo é paginate com um itemSelector (os itens são o que distingue uma página da lista de outra página que você abre nessa aba), a página onde a verificação estava abre no seu Chrome; para um paginador cujas páginas não têm endereço próprio, essa é a página da própria lista, e as páginas lidas antes da verificação são mostradas novamente no caminho sem contar contra maxPages. Você passa pela verificação lá, depois pagina você mesmo clicando em Próximo: o Octocrawl apenas lê essa aba (um script somente leitura; não clica em nada e não envia nada), e para quando Próximo sumiu, ficou oculto ou desabilitado na última página que leu por 5 s, no maxPages do passo, após 60 s sem uma nova página, ou no waitMs da transferência (padrão 10 minutos em todos); espere a palavra do terminal ou da ferramenta de que a página da verificação foi lida antes de clicar em Próximo, e não feche a aba (isso, ou sair do Chrome, abandona o item, páginas lidas incluídas); se você passou mas não mostrou nenhuma página após a verificação dentro desse tempo, o item mantém seu resultado interrompido e uma transferência posterior continua de lá. As páginas que o navegador próprio do Octocrawl leu antes da verificação e as páginas que você mostrou a ele são mescladas em uma lista, cada página uma vez (actions.scrapes[].by é user_browser para as suas); o item se torna a lista inteira, actions.lists[].continued é { from, pages, by: "user_browser" }, e stoppedBy diz como a leitura terminou (end, max, ou deadline com um aviso list_not_exhausted).

  • Onde é oferecido. Apenas em um servidor que roda na sua máquina e responde somente a você: um servidor que não está em loopback, e o serviço hospedado, respondem 409. Um lote que pediu a página actions ou um screenshot também não é transferido (409, sem handoff em seus itens): esses são para o navegador do Octocrawl pegar, e uma página lida no seu Chrome não pode fornecê-los. Cancelar enquanto o Chrome ainda pergunta "Permitir depuração remota?" derruba a conexão, então um Permitir clicado depois não se anexa a nada. A transferência roda em um lote finalizado, não enquanto ele roda. Ela não envia evento de webhook para um item que substitui: leia os itens novamente.

  • octocrawl serve (e npm run api) lê logins salvos apenas quando escuta em loopback, e então responde apenas a solicitações endereçadas a 127.0.0.1, localhost ou [::1] sem Origin estrangeiro, então outra máquina ou uma página web usando um nome DNS redirecionado não pode ler páginas como você. Escutando em outro endereço, ele não lê nenhuma e diz isso ao iniciar. Um servidor hospedado nunca as lê.

Seu próprio Chrome (faixa my-browser)

A depuração remota, que esta faixa precisa, faz toda página ver navigator.webdriver como true enquanto estiver ativada (veja a transferência acima). A única exceção é um lote cujo único passo é paginate com um itemSelector: um item cuja lista parou em uma verificação (actions.lists[].stoppedBy é challenge) é transferido como acima; seus outros itens interrompidos não são. Uma página que só precisa do seu login, seu endereço ou um navegador real é lida; um site cuja verificação de bot olha para navigator.webdriver pode recusar seu Chrome como recusa as faixas próprias do Octocrawl. Na execução de aceitação de 2026-10-07, studylib.net e imf.org foram lidos dessa forma, enquanto crunchbase.com (uma página de bloqueio do Cloudflare) e stackoverflow.com (um desafio do Cloudflare que não limpou) não foram; por que esses dois recusaram não está isolado.

Em um servidor rodando na sua máquina, um scrape pode ler sua página no seu próprio Chrome em vez de buscá-la: octocrawl scrape <url> --lane my-browser, ou "lane": "my-browser" em POST /v1/scrape, o scrape do SDK e a ferramenta MCP scrape. O Octocrawl não busca nada por conta própria; seu Chrome carrega a página, conectado como você onde você está.

  • Você permite isso duas vezes por conexão. O Chrome pergunta "Permitir depuração remota?" (depuração remota ativada, como para octocrawl login import). O Octocrawl então abre uma página própria no seu Chrome que lista o site e a tarefa, e aguarda você clicar em Permitir leitura destes sites lá. Somente esse clique, que o Chrome conta como seu, permite o site: um script não pode. Fechar essa página, ou clicar em Revogar, interrompe isso, e uma página sendo lida então termina como cancelled. Não permitir o site a tempo (10 minutos), fechar a página ou clicar em Revogar primeiro recusa a solicitação (409), e a recusa diz o que a página respondeu por último (não clicado, ou permitido sem um clique que o Chrome contou). Um lote aguardando você permitir seus sites diz isso em seu status (waitingForApproval: true), e o log do servidor mostra cada etapa da aprovação (my_browser_approval), nunca o conteúdo de uma página.
  • A página é lida sem um clique, somente nesse host. Ela é lida quando carregou, respondeu 2xx, não mostra verificação e está no host que você permitiu, exatamente: uma página que leva a um subdomínio ou a um domínio pai dele não é lida sem você, no entanto o site os vincula. Caso contrário, é julgada como um handoff julga uma página (três leituras seguidas). Seu conteúdo é lido como aparece no seu Chrome: o que a página esconde com display: none ou visibility: hidden, como um painel de ajuda ou uma aba não selecionada, é deixado de fora; se mostra uma verificação, e seu HTML bruto, são o documento inteiro. O que uma página desenha em um canvas, como uma planilha Lark desenha suas células, não está nela para ler. Uma página que mostra uma verificação aguarda você passar por ela (handoff.waitMs, padrão 10 min). Uma página não lida responde cancelled, blocked com a verificação que ainda mostrava, failed/connection_error quando o Chrome recusa um comando (uma aba que não abrirá), failed/redirect_limit quando o site leva a aba para outra página dele cada vez que o Octocrawl a retoma (duas vezes), ou failed/timeout, com um aviso my_browser_not_read que diz o porquê.
  • Como é registrado. Faixa my_browser, nunca em cache; compliance: null e usage.requestCount: 0 (Octocrawl não enviou nada), nenhuma decisão de robots.txt (não buscou nada), e o access do Registro de Evidências diz route: user_browser, o navegador que o leu, e completion: user_browser, ou handed_to_person quando a página mostrou uma verificação pela qual você passou.
  • Onde é oferecido. Como o handoff: somente em um servidor na sua máquina, respondendo somente a você; em outros lugares, e com página actions, um screenshot, lockdown ou um modo diferente de padrão, a solicitação é recusada pelo nome (unsupported_parameter).
  • Lotes. "lane": "my-browser" em POST /v1/batches, a ferramenta MCP batch_scrape ou octocrawl batch <urls> --lane my-browser lê cada página dessa forma, uma de cada vez. A página do Octocrawl no seu Chrome lista cada site do lote (host e porta) e você os permite uma vez para a execução; uma página em um site que não está entre eles não é lida, nem qualquer página depois que você os revoga. Não permiti-los responde a cada página cancelled, e o Chrome não alcançado responde failed/connection_error, cada um com o motivo. Uma execução retomada mais tarde (uma reinicialização, uma pausa) pede novamente: um servidor reiniciado enquanto tal lote rodava abre o prompt do Chrome ao iniciar, e páginas deixadas quando você não responde terminam cancelled. Uma URL anexada enquanto o lote roda, em um site que não está na lista dessa execução, termina cancelled também, e não é lida depois. Cancelar o lote, ou seu orçamento de tempo terminar, fecha a aba sendo lida e a conexão. Uma página aqui não tem timeout próprio (seu tempo é seu), somente o do lote. Um lote nesta faixa não aceita webhook (páginas lidas como você não são enviadas para outro lugar) e nenhum maxConcurrency acima de 1.

O access.completion de cada Registro de Evidências conta como uma página foi lida: unattended (as próprias faixas do Octocrawl), authorized_session (seu login salvo, modo authed), user_browser (seu Chrome, em um site que você permitiu, sem uma etapa sua) ou handed_to_person (seu Chrome, depois que você passou por uma verificação). É nulo quando nenhuma página foi lida.

Para pesquisadores, dois guias percorrem uma execução real: De uma lista de URLs a um CSV com evidências (a linha de comando e o cliente Python, cada coluna de evidências, e por que linhas com falha permanecem) e Citando dados da web em um artigo (uma seção de métodos, uma referência com sua data de acesso e hash, e dados pessoais).

As portas que os serviços locais escutam, todas em 127.0.0.1:

PortaServiçoIniciado porAlterado com
8787A API REST, e a API que o servidor MCP stdio (octocrawl-mcp) e o cliente Python chamam por padrão (o SDK TypeScript usa um baseUrl)octocrawl serve, npm run api--port, W2L_API_PORT; esses clientes W2L_API_URL
8791O serviço MCP local (API, agendador do Monitor, worker de entrega e endpoint MCP em /mcp)npm run local:mcp, ou o LaunchAgent de npm run local:mcp:installW2L_LOCAL_MCP_PORT
8788O receptor de webhook HTTPS do passo a passo de primeiro usonpm run first-use:localfixo
8798A pré-visualização local do site públiconpm run public:preview:localW2L_PUBLIC_PREVIEW_PORT

Para uso MCP, há três formas, da menos à mais configuração; as configurações para Cursor, OpenCode e Codex, e uma primeira tarefa, estão em Conectar MCP.

Hospedado (raspar e mapear; sem chave dentro de uma cota diária sobre HTTP, uma chave para mais páginas e a faixa de navegador; veja docs/hosted-api.md):

claude mcp add --transport http octocrawl https://mcp.octocrawl.dev/mcp
curl -sS -X POST https://api.octocrawl.dev/v1/scrape -H 'content-type: application/json' -d '{"url":"https://example.com"}'

No seu computador (tudo: raspar, mapear, rastrear, lote, a ferramenta de produto Amazon.sg e as ferramentas do Monitor; sem limite; nada deste checkout é necessário):

npx octocrawl serve                                  # keep it running: the API on 127.0.0.1:8787
claude mcp add octocrawl -- npx -y @octocrawl/mcp    # the published stdio server, a client of that API

Auto-hospedado para outros: npx octocrawl serve --hosted --token <token> escuta em todas as interfaces atrás de um token de portador, com endereços privados, sobreposições de robots, logins salvos, handoff e webhooks não-HTTPS recusados; aponte @octocrawl/mcp para ele com --base-url e --token (ou W2L_API_URL e W2L_API_TOKEN).

O serviço local gerenciado do checkout é para o fluxo de entrega Monitor → HTTPS: um serviço em segundo plano executa a API, o agendador do Monitor, o worker de entrega e um endpoint MCP Streamable HTTP em http://127.0.0.1:8791/mcp. No macOS, instale-o como um LaunchAgent e conecte o Codex à sua URL de loopback:

npm run local:mcp:install
codex mcp add w2l-local --url http://127.0.0.1:8791/mcp
npm run local:mcp:status

Ele reinicia após uma falha de processo e no login. Nenhuma hospedagem ou conta de login é necessária para este caminho local. npm run local:mcp:uninstall remove o agente; codex mcp remove w2l-local remove a entrada do cliente. Em outros sistemas, execute npm run local:mcp em um terminal. O estado permanece em .w2l/api por padrão. Veja o passo a passo de primeiro uso do MCP para o fluxo real do Monitor e entrega HTTPS e configuração de segredos. Mantenha este checkout enquanto o LaunchAgent aponta para ele.

Para receber eventos assinados no mesmo Mac com uma URL HTTPS de loopback fixa, execute npm run local:receiver:install, depois reinstale o serviço MCP com W2L_LOCAL_DELIVERY_LOOPBACK=1 npm run local:mcp:install. A opção somente permite entrega em loopback e fixa a confiança no certificado local gerado. O receptor e sua caixa de entrada SQLite rodam como um LaunchAgent separado; nenhum serviço se torna alcançável de outra máquina.

A API REST standalone legada permanece disponível para clientes SDK e Firecrawl-shim:

npm run api

Para conectar um processo MCP stdio standalone a essa API, execute:

npm run mcp

npm run api vincula 127.0.0.1 e permite loopback/RFC1918 para que servidores de fixture funcionem. O modo hospedado é explícito: npm run api -- --hosted --token $W2L_API_TOKEN. Isso vincula 0.0.0.0, exige Authorization: Bearer, nega IPs privados/metadados, e limita um rastreio a 100 páginas: um maxPages omitido ou null usa 100, e um maior é recusado com invalid_request. Ele obedece robots.txt para cada URL e não oferece como contorná-lo: robotsOverride, robotsOverrides e ignoreRobotsTxt são recusados com unsupported_parameter (veja abaixo).

Um servidor iniciado com tokens, hospedado ou local, aceita qualquer um deles: repita --token, ou defina W2L_API_TOKEN e o W2L_API_TOKENS separado por vírgulas. Tokens na linha de comando substituem os do ambiente. Um --token sem valor (o último argumento, seguido por outra flag, ou em branco) para o servidor na inicialização, e o erro nunca repete um token. Dê a cada cliente seu próprio token; reiniciar o servidor sem um token o revoga. Tokens são comparados como digests SHA-256 de comprimento fixo em tempo constante, e um token ausente ou desconhecido recebe HTTP 401 com { "error": "unauthorized", "code": "unauthorized" }. O SDK envia sua opção token, ou W2L_API_TOKEN do ambiente quando nenhum é passado; token: '' não envia nenhum.

Um operador pode limitar quantas solicitações que iniciam trabalho cada chamador pode fazer: W2L_RATE_LIMIT_PER_MINUTE=<n> ou --rate-limit-per-minute <n> (um inteiro de 1 a 100.000; não definido ou vazio significa sem limite, qualquer outra coisa interrompe a inicialização com W2L_RATE_LIMIT_PER_MINUTE must be an integer between 1 and 100000) conta POST /v1/scrape, /v1/crawl, /v1/batches, /v1/map, /fc/v1/scrape, /fc/v1/crawl e /fc/v1/map em uma janela deslizante de 60 segundos por token de portador (para o único chamador local quando o servidor não aceita token); leituras de status são gratuitas. Acima do limite, a resposta é HTTP 429 com um cabeçalho Retry-After (segundos inteiros, pelo menos 1) e { "error": "rate limit exceeded: <n> requests per minute", "code": "rate_limited", "retryAfterSeconds": <s>, "agentHints": ["wait <s> s before the next request"] }; /fc responde { success: false, error, code: "rate_limited", agent_hints } com o mesmo cabeçalho. rate_limited não é um dos códigos de erro de solicitação: a solicitação estava bem formada, o orçamento do chamador foi gasto. O SDK lança W2LError com status 429, code rate_limited, retryAfterMs lido do cabeçalho (delta-seconds ou uma data HTTP) e agentHints, e não tenta novamente nada, assim como os SDKs do Firecrawl não; chamadas de ferramenta MCP falham com rate limited: retry after <s> s (rate_limited). A janela está na memória e por processo, então uma reinicialização a redefine, e ela se baseia no digest do token, não no endereço do cliente, então tokens rotacionados têm orçamentos separados. Nada sobre polidez de saída muda: o limite por origem e os cooldowns Retry-After em direção a sites permanecem como estão.

Atrás de um proxy, o modo local (npm run api, o serviço MCP local, npm run scrape/crawl) envia suas solicitações de saída, incluindo robots.txt e o navegador local, através de HTTPS_PROXY para URLs https: e HTTP_PROXY para URLs http: (nomes em minúsculas também), com as regras do curl: hosts NO_PROXY e seus subdomínios, host:port, entradas IP e CIDR vão direto, * desativa o proxy, e loopback é sempre direto. O proxy deve ser http:// ou https://, e ambas as variáveis devem nomear o mesmo. O proxy resolve os nomes que busca, então para solicitações via proxy o Octocrawl confia nele para resolução e verifica somente a URL em si (esquema, credenciais, literais IP, nomes de metadados); solicitações diretas ainda são resolvidas, validadas e fixadas. Resultados nomeiam o host:port do proxy em evidence.envProxy e um evento de rastreamento egress_proxy, nunca suas credenciais. W2L_PROXY=off ignora as variáveis; o modo hospedado nunca as usa. Sem o proxy de ambiente, o navegador local conecta diretamente como a faixa HTTP: nunca recorre às configurações de proxy do sistema operacional, uma rota que nenhum resultado registraria. O LaunchAgent do macOS não herda seu shell, então coloque essas variáveis em .w2l/local-mcp.env. O Octocrawl verifica certificados por padrão, também através do proxy (o proxy tunela TLS de ponta a ponta); skipTlsVerification desativa isso para uma solicitação local, é registrado, e é recusado no modo hospedado (veja as opções de raspagem abaixo). O robots.txt é lido para cada URL que o Octocrawl busca, e seu veredito é registrado; o que ele decide depende de quem escolheu a URL (decidido em 2026-10-05). O robots.txt se dirige a rastreadores que descobrem links, então em um servidor local, uma URL que a solicitação nomeia (um scrape, uma entrada de lote, a lista de URLs da CLI, MCP scrape e batch_scrape, /fc/v1/scrape) é buscada independentemente do que o robots.txt diz, como seria uma visita de navegador: o resultado mantém o veredito (robotsDecision.decision: "disallowed", userOverride: true, overrideBasis: "user_named_url"), um aviso de robots_overridden e os eventos de rastreamento abaixo. Os links que um crawl ou mapa descobre obedecem ao robots.txt, a menos que o crawl ou mapa tenha sido iniciado com ignoreRobotsTxt em um servidor local (overrideBasis: "ignore_robots_txt"; um crawl então também lê os arquivos de sitemap que o robots.txt desautoriza, e um mapa retorna as URLs que ele desautoriza, com o robots de cada link dizendo disallowed ou unreachable). As releituras agendadas de um Monitor também o obedecem. Um servidor hospedado obedece ao robots.txt para cada URL, pois busca a partir dos endereços do operador. Um proprietário de site pode se dirigir ao próprio Octocrawl: linhas de User-agent do robots.txt são comparadas com o User-Agent da solicitação com o token de produto Octocrawl adicionado, em todos os modos, então um grupo para Octocrawl (ou o w2l-research do modo de pesquisa) o governa, qualquer que seja o cabeçalho enviado. Tal regra é a exclusão direcionada do proprietário: uma URL nomeada e ignoreRobotsTxt não a deixam de lado, e apenas um robotsOverride com seu motivo registrado o faz, em um servidor local. O ritmo não muda com nada disso: um lote e um crawl espaçam as páginas de um host pelo seu Crawl-delay, quer o robots.txt permita a página ou não, e um 429 esfria o host para cada solicitação. Um robots.txt com 4xx significa nenhuma restrição. Um robots.txt que não pode ser buscado (um 5xx, um erro de rede ou nenhuma resposta em 5 segundos) é uma desautorização completa, como o RFC 9309 §2.3.1.4 exige: onde o robots.txt é obedecido, a página não é buscada, o resultado é failed com policy_denied, e os eventos de rastreamento robots_checked e robots_disallowed e a decisão de robots do registro de conformidade (faixas de navegador e provedor) carregam unreachable: "server_error", "network_error" ou "timeout", para que nunca pareça uma regra que o editor escreveu. O Octocrawl pede esse robots.txt novamente após cinco minutos (robotsUnreachableTtlMs na política de rede). Em uma conexão direta, um nome que não resolve ainda é dns_error; através do proxy de ambiente, que resolve nomes por conta própria, sua solicitação de robots.txt falha primeiro, então a página é policy_denied com unreachable: "network_error". Onde uma URL é buscada além disso (uma URL nomeada, ignoreRobotsTxt, robotsOverride), o motivo inalcançável permanece no rastreamento e o aviso de robots_overridden diz que o arquivo não pôde ser lido. Em um servidor local, um scrape ou entrada de lote também pode carregar seu próprio motivo registrado (robotsOverride, descrito com as opções de scrape abaixo); ignoreRobotsTxt em um scrape ou lote é recusado com HTTP 400 nomeando-o.

O modo de pesquisa (mode: "research", --mode research na linha de comando) declara o Octocrawl como um bot em seu User-Agent. Defina W2L_CONTACT para dizer quem o executa, como um nome e endereço de e-mail ou uma URL, por exemplo W2L_CONTACT="Jane Doe jane@example.org": ASCII imprimível, no máximo 200 caracteres, sem parênteses ou barras invertidas. O User-Agent de pesquisa então termina com ; contact: Jane Doe jane@example.org). Para sec.gov e seus subdomínios, ele assume o formato que a política de acesso justo da SEC prescreve, <Company or name> <email>, em vez disso: W2L Research Jane Doe jane@example.org (então forneça um endereço de e-mail). O robots.txt deles é solicitado com ele também, um grupo de robots.txt para w2l-research ainda se aplica lá, e o identity.contact do Registro de Evidências lê o contato de qualquer um dos formatos. Os resultados mantêm o User-Agent que foi enviado (o evento de rastreamento identity_sent na faixa HTTP, o sentHeaders do registro de conformidade na faixa do navegador). O modo padrão envia um User-Agent de navegador e não declara contato. A SEC.gov responde 403 a clientes automatizados que não declaram contato; na faixa HTTP, tal 403 de um host da SEC carrega um evento de rastreamento declared_contact_hint dizendo para usar mode: "research" com W2L_CONTACT definido. npm run api, o serviço MCP local (em .w2l/local-mcp.env) e npm run scrape/crawl leem a variável.

O MCP local unificado cobre scrape, mapa, Crawl, lotes persistentes de array de URLs e Monitor/Delivery sem terminais de trabalho separados. Um serviço unificado também implementa Streamable HTTP autenticado para o Monitor de documento público revisado e fluxos anônimos de JSON/lote de produtos Amazon.sg (experimental; sua configuração está arquivada em docs/archive/hosted-mcp-pilot.md); a hospedagem está pausada no roteiro (ROADMAP.md). Para ambos os fluxos em um Mac, execute npm run first-use:local após npm ci; veja o guia de primeiro uso de dois fluxos, passo a passo de primeiro uso do MCP e status C2/C3. Clientes avançados podem ainda iniciar o adaptador stdio legado deste repositório:

{
  "mcpServers": {
    "w2l": {
      "command": "npm",
      "args": ["run", "mcp"],
      "env": { "W2L_API_URL": "http://127.0.0.1:8787" }
    }
  }
}

O MCP scrape é compacto por padrão: ele retorna o conteúdo selecionado, metadados de documento/produto, uso agregado e erros sem repetir o corpo sob summary.attempts, com o status da resposta e o cabeçalho content-type em snapshot (httpStatus, contentType) e a URL final e cada salto de redirecionamento em evidenceRecord. Passe debug: true quando precisar da rota completa, rastreamento e auditoria por tentativa. Chamadas REST e SDK que omitem formats e debug mantêm a resposta Markdown completa legada.

Para muitas URLs conhecidas, use batch_scrape no MCP, depois get_batch, get_batch_items ou wait_batch. REST e SDK suportam a mesma tarefa durável e itens paginados. REST transmite um crawl ou lote enquanto executa (GET /v1/crawl/:id/events e GET /v1/batches/:id/events como eventos enviados pelo servidor, /ws em cada um para um WebSocket), e o watcher(jobId, { kind }) do SDK segue um trabalho por essas rotas ou, onde estão desligadas, por polling (o parágrafo do observador abaixo). Veja raspagem em lote. O teto de concorrência por origem é configurável até quatro, com um resfriamento Retry-After compartilhado e intervalo mínimo de solicitação. A comparação controlada 1/2/4 é evidência de fixture local, não uma alegação de velocidade da Amazon. Um lote também aceita maxConcurrency (um inteiro de 1 a 4): o máximo de suas páginas em voo de uma vez, em todos os seus hosts. Ele apenas reduz a contagem de trabalhadores do serviço (4 localmente, 2 no host MCP hospedado; GET /v1/batches/:id relata o teto em vigor como maxConcurrency), o teto por host e o intervalo mínimo ainda se aplicam, e o teto é armazenado com a tarefa, então um lote retomado após uma reinicialização executa sob ele. ignoreInvalidURLs: true inicia o lote com as entradas de urls que são URLs http(s) e relata o resto como invalidURLs no 202 e no status, onde sem ele tal entrada recusa a solicitação inteira por seu índice (urls[2] must be http(s)); uma entrada que não é uma string, ou uma duplicata, é recusada de qualquer forma. Os sinalizadores de escopo de extração do Firecrawl allowExternalLinks e includeSubdomains são aceitos em um lote como false apenas, que já se mantém (um lote busca as URLs fornecidas e não segue nenhum link); true é HTTP 400 nomeando a opção de crawl que o faz (allowExternalLinks: true is not offered on a batch: a batch fetches only the URLs given; a crawl takes allowExternalLinks, and extraction across links is the M5 multi-URL extract; includeSubdomains aponta para o allowSubdomains do crawl). GET /v1/batches/:id relata succeeded (itens registrados success, partial ou empty_verified) e failed (os itens que a lista de relatórios de erros) ao lado de completed. GET /v1/batches/:id/errors (SDK getBatchErrors, MCP get_batch_errors) lista os itens que não tiveram sucesso em cada tentativa do lote, então um lote interrompido e retomado mantém suas falhas anteriores no registro, cada uma como { id, timestamp, url, status, code, error, httpStatus } (nomes do Firecrawl; code é o failureReason, blockReason ou budgetExceeded do item), em páginas de até 1000, com robotsBlocked, as URLs que o robots.txt recusou: um item policy_denied com um evento de rastreamento robots_disallowed que nenhuma substituição registrada deixou de lado. Uma recusa de governança ou SSRF é policy_denied também, mas não robots.txt, e permanece apenas em errors. Um lote também aceita idempotencyKey (1 a 200 caracteres sem caracteres de controle; também o cabeçalho x-idempotency-key ou Idempotency-Key que os clientes do Firecrawl enviam, em POST /v1/batches, POST /v1/crawl e /fc/v1/crawl; um início de crawl aceita o mesmo campo): uma submissão enviada novamente com a mesma chave e o mesmo corpo responde ao taskId da primeira submissão com replayed: true e não inicia nada, então uma nova tentativa após uma conexão perdida não busca cada URL duas vezes; a mesma chave com outro corpo é HTTP 409 conflict (idempotency key was used for a different request), uma chave de corpo que difere do cabeçalho é HTTP 400, e uma chave vive 24 horas, em um índice ao lado dos diretórios de tarefa (<task root>/idempotency.sqlite) mantido pelo processo de API único que executa essa raiz de tarefa. appendToId: "<taskId>" adiciona urls a um lote existente em vez de iniciar um novo trabalho (SDK appendToBatch(id, urls, options), MCP batch_scrape com appendToId): o trabalho mantém seu mode, formats, includeLinks, maxConcurrency e opções de página (enviar um é HTTP 400 appendToId keeps the job's options; formats cannot be changed; ignoreInvalidURLs, idempotencyKey e robotsOverrides para as novas URLs podem vir junto), as URLs vão para o final de sua lista (no máximo 1000 no total; uma URL já no lote é recusada pelo nome, appended url is already in the batch: <url>; uma URL em um novo host é buscada como as outras, sob suas próprias verificações de robots.txt e SSRF), um lote em execução as pega na mesma tentativa (nenhum trabalho é adicionado, então um limite de lote ativo não conta o anexo), um concluído executa novamente para elas em uma nova tentativa (ativo novamente, conta contra tal limite como um novo lote: em um servidor que executa um lote por vez, o anexo é HTTP 400 active batch limit reached enquanto outro lote está ativo, e o lote é deixado como estava), e um cancelado ou falho é HTTP 409 conflict (batch is cancelled); o 202 carrega requested (as URLs do trabalho agora) e appended, e GET /v1/batches/:id conta a lista mais longa. Para uma lista com mais de 1000 URLs, o batchScrapeChunked(urls, options, { chunkSize, itemLimit, pollIntervalMs, timeoutMs }) do SDK executa um lote por bloco de chunkSize URLs (padrão 100), cada um iniciado, aguardado e listado antes do próximo iniciar (um idempotencyKey do chamador se torna <key>:<chunk index> por trabalho), e retorna { jobs, items, invalidURLs } com os itens na ordem em que as URLs foram submetidas; chunkUrls(urls, chunkSize) divide uma lista sozinho. O batch_scrape do host MCP hospedado recusa idempotencyKey e appendToId como recusa as outras opções de lote.

Um crawl (POST /v1/crawl, MCP crawl) aceita o mesmo formats e includeLinks que o scrape, mais includePaths / excludePaths: expressões regulares comparadas com o caminho da URL de cada link descoberto. A URL inicial é sempre buscada e uma correspondência de excludePaths vence. Um padrão que pode sofrer retrocesso catastrófico em um caminho de link elaborado, como ^/(a+)+$ ou .*a.*b, é recusado com invalid_request: um grupo repetido com uma parte repetida e sem separador, uma escolha repetida cujas alternativas podem começar de forma semelhante, ou três ou mais partes repetidas sobrepostas em sequência. O resto executa no mecanismo de expressão regular de tempo linear do V8 onde pode executá-los; um com lookaround, uma referência inversa ou uma repetição contada acima de 16 (como {3,40}) executa no mecanismo de retrocesso, apenas em caminhos de até 2.048 caracteres e com um limite de 100 ms por link. Um link que um filtro não pode decidir não é seguido, e um filtro que ficou sem tempo não decide nenhum link posterior também. Scrape, lote e crawl rejeitam um campo desconhecido ou um formato não suportado com HTTP 400 nomeando-o. Um crawl segue links no host da URL inicial, no gêmeo www. desse host (example.com e www.example.com) e no host para o qual a URL inicial redireciona, e somente dentro da subárvore de caminho da URL inicial: uma URL inicial terminando em / limita o crawl àquele diretório, uma que nomeia um arquivo (/3/tutorial/index.html) ao diretório do arquivo, qualquer outra a si mesma e aos caminhos sob ela (/search admite /search?page=2 e /search/x, não /searching); um redirecionamento da URL inicial adiciona a subárvore da URL final. crawlEntireDomain: true segue links em qualquer lugar daquele host, como os crawls faziam antes da opção existir. allowSubdomains: true adiciona todos os hosts sob o ápice da URL inicial (o host com um www. inicial removido; não há lista de sufixo público, então uma URL inicial em www.gov.uk admite todos os hosts *.gov.uk), allowExternalLinks: true adiciona todos os hosts e não pode ser combinado com allowlistedDomains, e um allowlistedDomains não vazio adiciona os hosts que ele nomeia (exatos ou *.domain) ao próprio host da URL inicial. Hosts diferentes do da URL inicial não são limitados por caminho, e cada página em um novo host recebe sua própria leitura de robots.txt, verificação de SSRF e registro de identidade na lane que a atende. A governança segue a mesma regra: com allowlistedDomains a escada pode buscar o host da URL inicial, seu gêmeo, os hosts listados e *.apex sob allowSubdomains; caso contrário, apenas a fronteira limita o crawl. Um crawl não segue links cujo caminho termina em uma extensão de imagem, fonte, folha de estilo, script, áudio, vídeo ou programa (.png, .woff2, .css, .js, .mp4, .exe e similares); documentos e arquivos de dados como PDF, CSV, XLSX, JSON, XML e ZIP são seguidos.

includePaths / excludePaths correspondem ao pathname de um link; com regexOnFullURL: true eles correspondem à sua URL canônica (scheme://host/path?query, com o host em minúsculas, a porta padrão e parâmetros de rastreamento removidos e a consulta ordenada), então um padrão pode nomear um host ou uma consulta. ignoreQueryParameters: true trata URLs que diferem apenas na string de consulta como uma única página: a primeira variante vista é buscada (seu url mantém a consulta, seu canonicalUrl não) e as posteriores são relatadas como colapsadas. deduplicateSimilarURLs (padrão true) faz o mesmo para /a e /a/, / e /index.html (também .htm, .php), www. e o ápice, e http e https; o canonicalUrl de uma página permanece uma URL real, e um site que serve páginas diferentes em /a e /a/ precisa da opção desligada. Uma página buscada e depois descoberta como repetindo o corpo de uma página anterior (o mesmo rawBodySha256) mantém o status duplicate no checkpoint, é deixada de fora de GET /v1/crawl/:id/pages a menos que includeDuplicates=true (MCP get_crawl_pages, SDK getCrawlPages) e de fora do data de um status de crawl /fc, e ainda conta no total desse status. Todo crawl relata o que aconteceu com os links que encontrou: GET /v1/crawl/:id carrega discovery (offered, enqueued, duplicate, collapsed, hostDenied, subtreeDenied, pathDenied, depthDenied, duplicateContent; null para um lote), escrito após cada página, e o trace de cada página (pages?debug=true) carrega um evento discovered (via: seed ou link, from: a página que linka) e um evento links_offered com os contadores dessa página e até 20 links colapsados (url, into) e recusados pelo host como amostras. Uma tarefa de crawl armazena todas essas opções; uma armazenada antes de elas existirem retoma com sua regra original, o host inteiro e URLs canônicas exatas. O CLI octocrawl crawl ainda não tem flags para essas opções: ele continua seguindo o host inteiro (crawlEntireDomain) e assume os outros padrões.

Um crawl lê o sitemap do site por padrão (sitemap: "include", como o Firecrawl faz): os arquivos que o robots.txt da URL inicial nomeia em linhas Sitemap:, ou /sitemap.xml quando não nomeia nenhum. Um <sitemapindex> é seguido um nível, seus filhos em ordem listada; um arquivo .gz, ou um cujos bytes começam com o número mágico gzip, é inflado sob o limite de descompressão de 50 MiB; no máximo 20 arquivos são lidos por tentativa, e a carga para quando contém tantas entradas quanto maxPages (50 000 quando o crawl é ilimitado). As entradas vão para a fronteira na profundidade 1, após a URL inicial e à frente de seus links, sob as mesmas regras de host, subárvore, includePaths / excludePaths e maxDepth que um link (então maxDepth: 0 as descarta todas), então um crawl limitado de um site com sitemap retorna páginas diferentes do que antes da opção. sitemap: "only" não segue nenhum link de página: as páginas são a URL inicial e as entradas do sitemap, e os links de cada página ainda são retornados quando solicitados. sitemap: "skip" não lê nenhum. Um arquivo de sitemap é uma busca auxiliar como robots.txt, nunca uma página: é solicitado com a identidade http do próprio modo de crawl (seu User-Agent e client hints, os móveis quando as páginas do crawl os declaram, e nada que um chamador adicionou), após a verificação de SSRF em sua URL e em cada salto de redirecionamento, através da mesma rota fixada por DNS ou proxy do operador, ritmado pelo agendador de origem, dentro do limite de redirecionamento da política e do limite de 10 MiB de fio, e somente após sua própria URL passar pelo robots.txt do host sob essa identidade (um robots.txt desautorizado ou inacessível o recusa, como faria com uma página, a menos que o crawl tenha sido iniciado com ignoreRobotsTxt). Ele nunca passa pela escada ou pelo extrator, e não tem registro de conformidade assinado: o registro dessas buscas é o discovery.sitemap do relatório (mode, sources: robots ou guess, files, listed, enqueued, truncated, error), onde cada arquivo carrega seu url, finalUrl, status, contentType, bytes, sha256, kind (index, urlset, absent para um 4xx, not_sitemap para um corpo 2xx que não é nenhum, unreadable com o motivo, como body_too_large ou decompressed_too_large, ou refused), entries, veredito robots e proxyUsed. Um 4xx ou um corpo HTML nunca é um erro; um filho ilegível é registrado e a carga passa para o próximo. Buscas de sitemap são espaçadas pelo intervalo mínimo do agendador; o Crawl-delay do robots.txt governa os inícios de página do crawl desde a primeira página. Uma página encontrada através do sitemap carrega discovered { via: "sitemap", from: <the file's URL> } em seu trace, e as entradas do sitemap contam em discovery ao lado dos links. O CLI octocrawl crawl ainda não lê sitemap, e uma tarefa de crawl armazenada antes da opção retoma sem um.

maxConcurrency limita as páginas que um crawl busca de uma vez: um inteiro de 1 até a contagem de workers do serviço (W2L_WORKER_COUNT, 4 por padrão em uma API local; 2 no host MCP hospedado; um valor maior é recusado com maxConcurrency must be at most N on this service). Ele apenas reduz o paralelismo de um crawl: o teto por origem (W2L_PER_HOST_CONCURRENCY, no máximo 4) e o intervalo mínimo ainda se aplicam, então nunca eleva o teto do host. A evidência da configuração está nas próprias páginas: o createdAt e o usage.wallMs de cada página dão seu intervalo de busca. Uma retomada mantém o valor.

GET /v1/crawl/active (SDK getActiveCrawls(), MCP list_active_crawls, apenas local) lista os crawls que este processo de API está executando, os que iniciou e os que retomou na inicialização, início mais antigo primeiro: { crawls: [{ id, url, status, startedAt, pagesFetched, options }] }, onde options são as opções armazenadas do crawl (maxPages, maxDepth, allowlistedDomains, includePaths, excludePaths, useCached, sitemap, as opções de escopo de URL, maxConcurrency e scrapeOptions, as opções por página com formats e includeLinks). É sempre 200, { "crawls": [] } quando nada está executando; um lote nunca é listado (tem GET /v1/batches/:id), e não há id de equipe, já que o Octocrawl não tem equipes. A lista é dos crawls deste processo: um crawl que outro processo executa na mesma raiz de tarefa não está nela. Um crawl ou lote leva webhook (REST, SDK, MCP crawl e batch_scrape): onde o job publica seus eventos como entregas duráveis e com nova tentativa, uma string de URL ou { url, headers, metadata, events, secretEnv }. Cinco eventos: started (sequência 0) quando o job é aceito, um page por página registrada (todo resultado: sucesso, parcial, falha, bloqueado, duplicado; o page do payload é a página como GET /v1/crawl/:id/pages ou /v1/batches/:id/items a lista, trace vazio, sem auditoria, seu json incluído quando um formato json foi solicitado), depois completed, failed ou cancelled com o status do job como GET /v1/crawl/:id ou GET /v1/batches/:id o reporta então (report, e error em failed). events os restringe (padrão todos os cinco; um evento filtrado nunca é enfileirado, então nada pendente ou em dead-letter aparece para ele; um job cancelado é cancelled, nunca failed). Cada payload é { schemaVersion: "w2l.job-event/v1", eventId, sequence, jobId, jobKind: "crawl" | "batch", event, at, metadata, page?, report?, error? }: eventId é <taskId>:started, <taskId>:page:<stepId> ou <taskId>:<status> (um job que roda novamente, um crawl retomado ou um lote anexado após a conclusão, sufixa seu evento terminal posterior com o id da tentativa), ou <taskId>:handoff:<stepId> para um item de lote que um handoff substituiu pela página à qual a pessoa chegou, em um lote registrado antes de 2026-10-05 (um lote com webhook não é mais entregue: uma página lida no Chrome da pessoa é lida autenticada como ela), e sequence conta os eventos do job em ordem, 0 e depois um por página, evento terminal e de handoff, então um job de n páginas termina em n+1 e cada item entregue depois adiciona um; metadata é o da solicitação (no máximo 32 strings de no máximo 1000 caracteres, 8 KiB no total), {} quando nenhum, copiado em cada payload quando o evento é enfileirado, então uma nova tentativa reenvia o corpo idêntico. Cada solicitação carrega content-type: application/json, x-w2l-event-id, x-w2l-event-version (a sequência) e x-w2l-delivery-id, além de x-w2l-timestamp e x-w2l-signature (sha256= HMAC sobre <timestamp>.<body>) quando secretEnv nomeia uma variável de operador W2L_WEBHOOK_SECRET_* (nunca um segredo literal), e o headers da solicitação (no máximo 32, 8 KiB no total, nomes de tokens RFC 7230 em minúsculas, sem quebras de linha; content-type, content-length, host, connection, transfer-encoding e todo nome x-w2l-* são recusados pelo nome, webhook.headers: content-length is reserved), enviado em cada tentativa, novas tentativas incluídas, após os próprios do Octocrawl, que eles nunca sobrescrevem. Os valores de cabeçalho são armazenados no banco de dados de controle (section-b-control.sqlite, modo 0600) sozinhos: a tarefa, o status e cada rota de entrega mostram apenas seus nomes (headerNames), então um token bearer para o receptor pertence lá e em nenhum lugar em uma resposta; secretEnv continua sendo o caminho de assinatura recomendado. Um evento terminal é enfileirado somente após a linha da tarefa ser gravada como terminal, então uma entrega completed chega depois que GET /v1/crawl/:id já diz completed. O destino é job:<taskId> (GET /v1/delivery/destinations?jobId=<taskId>); GET /v1/deliveries?jobId=<taskId> e GET /v1/deliveries/page?jobId= listam as entregas, cada uma com suas tentativas em GET /v1/deliveries/:id, repetidas com backoff e Retry-After e em dead-letter após o orçamento de tentativas como as de um Monitor (POST /v1/deliveries/:id/retry reproduz uma), e o status do job reporta webhook: { destinationId, url (origin and path, no query), events, pending, delivered, deadLetter }. Os ids de eventos são determinísticos e uma entrega é única por evento, então uma retomada ou reinício oferece cada página persistida novamente e não envia nenhuma duas vezes, e um job concluído cujos eventos uma falha interrompeu é completado quando a API inicia, um item de lote que um handoff substituiu incluído; um evento oferecido novamente assume o número que teria tido, ou, quando outro evento detém esse número, o próximo após todo número enviado. O receptor deve ser https; um servidor local (npm run api, o serviço MCP local) também aceita http simples para um receptor de loopback (127.0.0.1, ::1, localhost, enviado direto via node:http), como servidores de fixture são permitidos, e recusa qualquer outra URL http com HTTP 400 webhook.url must be https (http is accepted only for a loopback receiver of a local service); um servidor hospedado (--hosted) recusa todo receptor http da mesma forma e um endereço privado ou de metadados com webhook.url must be a public address, e coloca em dead-letter um nome que resolve privadamente (webhook egress denied: <violation>). w2l-api os entrega ele mesmo: roda o worker de entrega que o runtime MCP roda, sob a política de entrega que imprime no início (TLS sempre verificado, o HTTPS_PROXY do shell nunca usado, W2L_DELIVERY_PROXY_URL para um proxy explícito, W2L_DELIVERY_CA_FILE confiável, W2L_DELIVERY_PRIVATE_ALLOWLIST para receptores https privados em modo hospedado), então npm run delivery:worker não é mais necessário ao lado dele, e um segundo worker no mesmo banco de dados de controle é seguro, já que uma entrega é arrendada e cercada. O batch_scrape do host MCP hospedado recusa webhook (unsupported remote tool option) e não oferece crawl. Em /fc/v1/crawl, o webhook do Firecrawl (uma string ou { url, headers, metadata, events }) é mapeado para a opção nativa e seu receptor recebe a forma do Firecrawl, { success, type: "crawl.started" | "crawl.page" | "crawl.completed" | "crawl.failed", id, data: [page], metadata, error? } (um crawl cancelado é crawl.failed com error: "cancelled"), a identidade de evento do Octocrawl nos cabeçalhos (docs/firecrawl-shim.md). Uma entrega é contabilidade sobre o job: nada sobre o fetch, trace ou registro de conformidade de uma página muda com um webhook.

O SDK segue os cursores de uma listagem para você: listCrawlPages(id, options) e listBatchItems(id, options) aceitam, além de limit, attemptId, debug e includeDuplicates, os limites maxPages (páginas lidas após a primeira), maxResults (itens no total) e maxWaitMs (nenhuma página adicional após esse tempo), e param silenciosamente quando um é alcançado; o valor de retorno do gerador diz onde (nextCursor, stoppedBy: end, maxPages, maxResults ou maxWait). Com maxResults, cada página é solicitada não maior do que o que ainda é desejado, então o cursor onde a listagem para continua exatamente após o último item retornado. getCrawlDocuments(id, options) retorna { report, pages, nextCursor, stoppedBy } e getBatchDocuments(id, options) { report, items, nextCursor, stoppedBy }, o status e os documentos em uma resposta, todo documento a menos que um limite pare a listagem; collectCrawlPages e collectBatchItems retornam apenas os documentos. Uma listagem é da tentativa mais recente, a menos que attemptId nomeie outra (uma retomada com useCached re-registra as páginas que reutiliza em sua nova tentativa, então essa tentativa normalmente detém toda página). Os tamanhos de página do servidor são inalterados: páginas de crawl 1 a 1 000 por solicitação (padrão 50), itens de lote no máximo 50. MCP get_crawl_pages e get_batch_items aceitam maxResults (1 a 200) e depois seguem os cursores eles mesmos, respondendo { items, nextCursor, hasMore, stoppedBy }; get_crawl_pages também aceita includeDuplicates. O status de crawl do shim /fc ainda pagina com next sozinho.

Uma tarefa de crawl armazena toda opção com a qual foi iniciada. Um crawl pausado por um desligamento ou deixado em execução por uma falha retoma quando a API inicia novamente, POST /v1/crawl/:id/resume (SDK resumeCrawl, MCP resume_crawl) reinicia um pausado ou falho, e octocrawl crawl --resume <taskId> continua um da linha de comando; todos os três rodam com as opções armazenadas. Uma retomada refaz o fetch das páginas que o crawl já tem, ou as reutiliza quando o crawl foi iniciado com useCached: true (apenas as páginas próprias do crawl; maxAge reutiliza o cache entre solicitações). maxPages conta as páginas distintas da tarefa entre retomadas, então um crawl retomado nunca a excede. GET /v1/crawl/:id conta páginas enquanto o crawl roda; itens /v1/crawl/:id/pages carregam a auditoria de roteamento e o trace apenas com debug=true, como itens de lote. Entre dois inícios de página em um host, o crawl espera o robots.txt Crawl-delay desse host ou o intervalo mínimo (W2L_PER_HOST_MIN_DELAY_MS), o que for maior, e até que uma página em um host tenha reportado seu robots.txt, o crawl inicia uma página por vez lá. Cada página buscada registra a espera em um evento de trace crawl_delay: startedAt, previousStartedAt, observedDelayMs, requiredDelayMs e robotsCrawlDelayMs.

Para esperar um crawl ou lote do SDK, waitCrawl(taskId) e waitBatch(taskId) fazem polling até que esteja concluído, falho ou cancelado (uma tarefa pausada ainda é aguardada) e retornam seu status; crawlAndWait(url, options, wait) e batchAndWait(urls, options, wait) iniciam a tarefa, esperam e também retornam toda página e erro do crawl, ou todo item do lote. As opções de espera são pollIntervalMs (padrão 500), timeoutMs (sem limite por padrão), maxRetries (padrão 5) e signal. Quando timeoutMs se esgota, uma solicitação de status ainda em voo incluída, a espera lança WaitTimeoutError com taskId, timeoutMs e last, o último status lido (null quando nenhum respondeu a tempo), e a tarefa continua rodando. Uma solicitação de status que falha com erro de rede, HTTP 408, 429 ou 5xx é repetida após 1, 2, 4, 8, depois 10 s, ou após seu Retry-After quando isso pede 60 s ou menos; a espera lança qualquer outro erro imediatamente, e um transitório uma vez que maxRetries novas tentativas consecutivas falharam. Para acompanhar um job enquanto ele é executado, em vez de esperar por ele, GET /v1/crawl/:id/events e GET /v1/batches/:id/events o transmitem como eventos enviados pelo servidor: catchup com o relatório quando o stream abre, um document por página registrada, qualquer que seja o resultado (a página compacta GET /v1/crawl/:id/pages ou /v1/batches/:id/items lista, rastreamento vazio, sem auditoria, cada tentativa do job, cada etapa uma vez; seu id: é o cursor de etapa que as rotas de listagem usam), um snapshot com o relatório após cada página, então done com o relatório final, após o qual o stream fecha; error carrega { code, message }. ?after=<cursor> ou um cabeçalho Last-Event-ID retoma após um documento (um cursor que a API não emitiu é HTTP 400 cursor is not one this API issued); um id que não é um job é 404. GET /v1/crawl/:id/ws e GET /v1/batches/:id/ws fazem upgrade para um WebSocket que carrega os mesmos frames como JSON ({ type, data | error, cursor? }), fecha com 1000 após done, com 4404 para um job ausente e 4400 para um cursor inválido; em um servidor com tokens, o upgrade apresenta o token bearer no cabeçalho Authorization ou, onde a API WebSocket não oferece cabeçalho, como o subprotocolo w2l.token.<token>, ecoado de volta como o protocolo selecionado (nunca na URL; um token com caracteres que um subprotocolo não pode carregar usa a rota SSE). O servidor lê o checkpoint uma vez por assinante quando o stream abre e depois uma vez por página para todos os assinantes daquele job juntos; um stream é uma visão do job, não registra nada, e W2L_JOB_STREAMS=off transforma todas as quatro rotas em 404. O watcher(jobId, { kind: 'crawl' | 'batch', transport, pollIntervalMs, timeoutMs, after, signal, WebSocket }) do SDK retorna um JobWatcher (um EventTarget): eventos document (CustomEvent<CrawlPage>), snapshot, done (o relatório) e error ({ code, message }), também gerados por for await (const event of watcher); campos jobId, kind, status, data (cada documento, cada um uma vez) e transport; close() para de observar e deixa o job em execução. transport: 'auto' (padrão) tenta a rota WebSocket, depois eventos enviados pelo servidor, depois polling (getCrawlPages com includeDuplicates, getCrawlErrors e getBatchItems com o cursor do último documento, o status a cada pollIntervalMs, padrão 2000, no mínimo 250: um valor menor é um TypeError), alternando uma vez por nível quando um transporte está indisponível (um 404 nas rotas de stream, sem construtor WebSocket) ou termina antes de done, e continuando do último cursor, então um documento é emitido uma vez por id de etapa entre recuperação, entrega ao vivo e qualquer alternância; um 401 ou 403 é final (error unauthorized), e timeoutMs encerra a observação com error { code: "watcher_timeout", message: "job <id> did not finish within <ms> ms (last status: <status>)" } enquanto o job continua em execução. crawlAndWatch(url, options, watch) e batchScrapeAndWatch(urls, options, watch) iniciam o job e retornam seu observador. MCP não tem superfície de streaming (apenas requisição e resposta): wait_batch e get_batch_items são o caminho lá. O shim congelado v1 do Firecrawl não adiciona caminho WebSocket.

Um mapa (POST /v1/map, SDK map(url, opts)) lista as URLs de um site sem buscar cada página: ele lê o robots.txt do host inicial, no máximo um corpo de página (a URL inicial, apenas no degrau http; nenhum navegador é iniciado) e os sitemaps que o site declara (as linhas Sitemap: do robots.txt da URL inicial, senão /sitemap.xml; um índice é seguido um nível, arquivos .gz são inflados, no máximo 50 arquivos), dentro de um prazo. Ele aceita url, mode (standard ou research; authed é recusado: um mapa lê sitemaps públicos e uma página pública), limit (um inteiro de 1 a 100.000, padrão 5.000, o padrão e máximo documentados do Firecrawl), timeout (milissegundos, 1.000 a 300.000, padrão 60.000, para o mapa inteiro), as opções abaixo, ignoreRobotsTxt (acima; apenas um servidor local), origin e integration; qualquer outra chave é recusada pelo nome com unsupported_parameter (useIndex com a dica de que o Octocrawl não mantém índice de URLs; uma opção de página como headers, mobile ou skipTlsVerification, então um mapa não tem nada para afrouxar; o allowSubdomains e allowExternalLinks do crawl, que um mapa chama de includeSubdomains e não oferece). Cada candidato passa pelas regras de escopo do crawl (o host inicial, seu gêmeo www. e para onde a URL inicial redireciona; a subárvore de caminho da URL inicial; ativos deixados de fora; URLs similares dobradas, como deduplicateSimilarURLs faz em um crawl, exceto que um link visto primeiro sobre http: dá lugar à sua variante https: quando essa variante também chega, em uma origem cujo robots.txt o mapa leu de qualquer forma e que a permite (nenhum robots.txt adicional é lido para ela); a URL inicial permanece como fornecida) e o robots.txt do seu host sob a identidade declarada do mapa: uma URL desautorizada, ou uma cujo robots.txt não pode ser lido, não é retornada a menos que o mapa tenha sido iniciado com ignoreRobotsTxt, e o robots.txt é lido para o host inicial e no máximo 20 outros. A resposta (HTTP 200) é { id, url, status, stoppedBy, links, sources, refused, identity, warnings, agentHints?, elapsedMs }: links contém a URL inicial, depois os links da página inicial em ordem de documento, depois entradas de sitemap ainda não encontradas, cada uma { url, title?, description?, titleSource?, via, sitemapFile?, lastmod?, robots }. Um título nunca é inventado: o da URL inicial é o <title> da sua página (e apenas ela tem um description), o de um link de página é seu texto de âncora (espaços em branco colapsados, no máximo 300 caracteres; senão aria-label, title ou o alt de uma imagem interna), o de uma entrada de sitemap é seu <news:title>; nenhuma outra URL é buscada para um. via diz como uma URL foi encontrada (start, link, sitemap; uma URL encontrada de ambas as formas é um link), lastmod é o do sitemap como escrito. sources registra a leitura da página inicial (httpStatus, status, robots, rawBodySha256, linksFound) e cada arquivo de sitemap (como em um crawl), e refused conta o que foi deixado de fora e por quê (duplicate, collapsed, hostDenied, subtreeDenied, pathDenied, assetDenied, robots, robotsUnchecked, searchFiltered, overLimit, com amostras; uma vez que os links da página inicial sozinhos preenchem limit o mapa responde imediatamente e não lê sitemap, então overLimit então conta apenas os candidatos vistos antes de parar). status é completed quando cada fonte foi lida ou está definitivamente ausente (atingir limit é concluído, com stoppedBy: "limit"), partial quando links voltaram mas o prazo cortou a execução ou uma fonte falhou, e failed quando nada voltou e uma fonte falhou ou o prazo disparou; no prazo, a resposta ainda é HTTP 200 com o que foi encontrado, stoppedBy: "timeout" e um aviso map_timeout que nomeia os links encontrados e os arquivos de sitemap não lidos, nunca um 408 e nunca uma lista de aparência completa. Outros avisos: start_page_unreadable, start_page_client_rendered (a pista http encontrou a página preenchida por script; a dica é raspá-la com formats: ["links"]), sitemap_unreadable, sitemap_files_capped, robots_host_cap, robots_unreachable (um robots.txt que não pôde ser lido, que conta como desautorização completa; o aviso nomeia o host e o motivo, e uma URL inicial em tal host tem sources.startPage.robots: "unreachable" com robotsUnreachable, nunca disallowed, que é mantido para uma regra que o editor escreveu). Cada mapa é registrado em <taskRoot>/maps/<id>.json antes de ser respondido e relido com GET /v1/maps/:id (SDK getMap(id)); um cliente que desconecta cancela o mapa e não deixa registro. Um servidor hospedado aceita limit até 5.000 e timeout até 60.000, e recusa uma URL que sua política de canal atende apenas com a pista do navegador. O SDK espera timeout + 5000 ms pela resposta. O que um mapa não faz, contra o Firecrawl: sem índice de URLs, então um site sem sitemap mapeia apenas os links da sua página inicial (docs.python.org/3/ deu 24 links em 2026-10-03: seu sitemap lista 8 raízes de versão); search filtra e não classifica; um título é texto de âncora, não o <title> do alvo; sem description exceto o da URL inicial; sem location.

As opções de um mapa: sitemap é include (padrão: os links da página inicial e os sitemaps), skip (nenhum arquivo de sitemap é solicitado; os links são a URL inicial e os links da sua página, sources.sitemap é nulo) ou only (nenhum corpo de página é lido, sources.startPage é nulo; os links são as entradas de sitemap que o escopo, robots.txt e search admitem, em ordem listada, até limit, e a URL inicial está entre eles apenas quando um sitemap a lista; nenhum sitemap lido, ausente incluído, é failed com sitemap_unreadable). Com only a política de canal somente navegador do operador não recusa a URL, já que nenhuma página é lida. search (uma string de 1 a 200 caracteres com no máximo 10 palavras, após trim; senão HTTP 400 search must be a string of 1 to 200 characters with at most 10 words) mantém uma URL quando cada palavra aparece, sem diferenciar maiúsculas de minúsculas, em sua URL decodificada por percentual ou em seu título em mãos (o da própria página inicial, o texto de uma âncora, o <news:title> de um sitemap); ele roda antes de robots.txt e limit, então limit conta correspondências, mantém a ordem de descoberta (Firecrawl ordena por relevância; Octocrawl filtra), não busca nada mais, e conta o que deixou de fora em refused.searchFiltered. includeSubdomains (padrão false; Firecrawl v2 documenta true) admite todo host sob o ápice da URL inicial, a regra allowSubdomains do crawl: o host inicial com um www. inicial removido e sem lista de sufixo público; o robots.txt de cada novo host é lido uma vez sob a identidade do mapa, para no máximo 20 hosts além do host inicial, e as URLs em hosts adicionais são contadas em refused.robotsUnchecked com um aviso robots_host_cap. Sem ele, todo link está no host inicial, seu gêmeo www. (python.org e www.python.org são um site) ou o host para o qual a URL inicial redirecionou. ignoreQueryParameters (padrão false; Firecrawl v2 documenta true) dobra URLs que diferem apenas na string de consulta na primeira vista e a retorna sem sua consulta; cada dobra é contada em refused.collapsed com até 20 amostras { url, into }, nunca mesclada silenciosamente. Sem ele, variantes de consulta permanecem separadas, com parâmetros de rastreamento (utm_*, gclid, ...) removidos e o resto ordenado. O includePaths do crawl, excludePaths (no máximo 1.000 regexes de 1 a 2.000 caracteres, exclusão vence; um padrão que pode sofrer retrocesso catastrófico é recusado), regexOnFullURL, crawlEntireDomain (levanta a subárvore de caminho da URL inicial) e deduplicateSimilarURLs (padrão true) se aplicam com seus nomes, mensagens e regras de crawl. MCP tem o mesmo mapa que a ferramenta local map (anotada somente leitura, idempotente, mundo aberto, com um esquema de saída): compacta por padrão, { id, status, stoppedBy, links: [{ url, title?, description? }], warning?, agentHints?, counts: { returned, refused } } com as mensagens dos avisos unidas, a resposta nativa com debug: true, cada uma respondida como texto e como structuredContent; o host MCP hospedado não a oferece, já que não aceita URL arbitrária. /fc/v1/map aceita a requisição de mapa do Firecrawl (shim).

Scrape, batch e crawl também aceitam doze opções de página e as quatro opções de cache (abaixo); batch e crawl as aplicam a cada página e as armazenam com a tarefa, então uma tarefa retomada as mantém:

  • onlyMainContent (padrão true). false retorna o Markdown da página inteira: o corpo do documento com scripts, estilos, controles de formulário e mídia incorporada omitidos, e o cabeçalho, a navegação e o rodapé mantidos, através do mesmo conversor e URL base. As evidências (hashes, status) são as mesmas em ambos os modos, o roteamento de faixa ainda lê o conteúdo principal, e o evento de rastreamento extract registra onlyMainContent: false. Uma exceção: em uma página onde o extrator não encontra nenhum bloco principal, o padrão é failed/empty_unverified com o Markdown da página inteira mantido como evidência, enquanto false retorna esse Markdown como success; de qualquer forma, a faixa do navegador ainda é tentada e responde quando renderiza mais. links sempre vêm da página inteira.
  • includeTags (até 100 seletores CSS de 1 a 200 caracteres, com no máximo 100 partes de seletor no total; veja abaixo). O conteúdo são os elementos nomeados pelos seletores, em ordem de documento, um elemento dentro de outro nomeado apenas uma vez. Eles são retirados da página como foi recebida, antes da seleção e limpeza do conteúdo principal, então uma navegação, cabeçalho ou rodapé nomeado permanece, e onlyMainContent não escolhe mais o conteúdo; scripts, estilos, controles de formulário e mídia incorporada são omitidos como sempre. O tipo da página, título, metadata e links ainda são lidos da página inteira, a extração JSON ainda lê os fatos próprios da página e os pares rótulo/valor do seu conteúdo principal, e document.confidence é 1 quando os elementos nomeados contêm qualquer texto ou imagem, já que o que você nomeou é o conteúdo. Quando nada é nomeado, a resposta é success com Markdown vazio da faixa que leu a página, não uma falha; a faixa do navegador é tentada apenas quando a página em si é lida como fina ou cheia de scripts, como para qualquer página assim; quando essa faixa então encontra a página bloqueada, o bloqueio é a resposta e a vazia é abandonada (a auditoria da escada registra ladder_empty_answer_dropped), e quando ela também não nomeia nada, a resposta vazia é confirmada (confirmsEmpty em seu ladder_step) e nenhuma faixa adicional, incluindo a de um fornecedor, é consultada. Uma página bloqueada permanece blocked independentemente do que os seletores nomeiam, em qualquer faixa onde o bloqueio é encontrado: o título de uma parede de login não é a página que foi solicitada. Nomear body nomeia a página inteira.
  • excludeTags (o mesmo tipo de lista). Os elementos nomeados pelos seletores são removidos, com tudo dentro deles, antes de o conteúdo ser retirado: do conteúdo principal, da página inteira (onlyMainContent: false), de uma seleção includeTags, e da página que um resultado falho ou bloqueado mantém como evidência. Ambas as listas são comparadas com a página inteira como foi recebida, então footer p em excludeTags remove os parágrafos do rodapé de uma seleção includeTags: ["p"]. O evento de rastreamento extract registra ambas as listas.
  • waitFor (milissegundos, um inteiro de 0 a 60 000, padrão 0). A faixa do navegador espera esse tempo após a página carregar e estabilizar, então a captura; um documento para o qual a página avançou enquanto isso (um script, um meta refresh) estabiliza antes da captura, e o resultado relata a URL e o status desse documento. A faixa HTTP não pode executar scripts, então uma solicitação com waitFor começa na faixa do navegador, e a auditoria da escada registra a faixa pulada (ladder_channel_skipped). Onde nenhuma faixa do navegador está configurada, o resultado é failed com policy_denied e um evento de rastreamento wait_for_unavailable, nunca uma resposta que ignorou a espera. Sem waitFor ou actions, uma página que estabilizou é lida imediatamente, a menos que tenha pouco texto (no máximo 4.000 caracteres) e ainda mostre que seus dados estão a caminho: um elemento visível marcado aria-busy="true", ou um texto curto visível, não em um botão ou link, que seja ou termine em uma mensagem de carregamento ("Carregando...", "Buscando resultados...", "Aguarde"). Tal página é aguardada por até 8 s no total (dentro do timeout da solicitação), na faixa do navegador e na de um fornecedor, até o sinal desaparecer; o rastreamento diz quanto tempo e se desapareceu (loading_wait), e uma página lida como conteúdo enquanto ainda mostrava um sinal carrega um aviso page_still_loading. Barras de progresso e estilos de esqueleto não são considerados sinais, pois páginas concluídas também os usam (um histograma de avaliações, uma barra de idiomas), e nem um carregador deixado em uma página com mais texto (mais comentários, a próxima página de um feed).
  • timeout (milissegundos, um inteiro de 1 000 a 300 000, padrão 300 000). O prazo para toda a raspagem, incluindo waitFor. Quando dispara, a API ainda responde HTTP 200: partial com o melhor conteúdo que uma faixa produziu até então (por exemplo, o conteúdo HTTP enquanto a faixa do navegador ainda carregava), ou failed com failureReason: "timeout" quando nada utilizável existe. Ambos carregam usage.deadlineExceeded: true e um evento de rastreamento deadline_exceeded. Quando um waitFor passaria do prazo, o navegador para de esperar cerca de um segundo antes e captura a página como está então: partial quando essa página tem conteúdo, caso contrário failed/timeout. As esperas das faixas por um servidor lento seguem o timeout que você definiu: a faixa HTTP espera pelos cabeçalhos de resposta e por cada bloco do corpo, e o navegador espera pela navegação, até o prazo. Sem um timeout, eles mantêm seus padrões dentro do prazo de 300 000 ms: 10 s para cabeçalhos, 30 s entre blocos do corpo e 20 s para navegação. Uma faixa que para em um desses falha com timeout, sem usage.deadlineExceeded, e a escada não avança para a próxima faixa para ela: a resposta é o conteúdo que uma faixa anterior produziu, ou aquele failed/timeout. O evento de rastreamento navigate da faixa do navegador registra a espera que permitiu (timeoutMs). Um cliente que desconecta ainda cancela a raspagem. Um cliente MCP que cancela sua chamada scrape (notifications/cancelled), ou fecha a solicitação HTTP da chamada antes do resultado, também a cancela, via stdio e via serviços HTTP locais e hospedados; via HTTP apenas o mesmo cliente pode cancelar uma chamada (o mesmo ID de sessão, e no serviço hospedado o mesmo token de portador), veja cancelando uma chamada MCP. O scrape do SDK espera pela resposta até timeout mais 30 s; no Node, isso substitui a espera de 300 s do próprio fetch pelos cabeçalhos de resposta, que uma resposta em um prazo de 300 000 ms pode exceder. A extração JSON lê campos de uma página partial mas a reporta incomplete com um problema page_partial, e nunca chama o modelo para ela.
  • maxFileBytes (bytes, um inteiro de 1 até o W2L_MAX_FILE_BYTES do servidor). Um limite de tamanho menor para um arquivo (PDF, CSV, XLSX, ZIP, JSON, texto) do que o do servidor; veja Arquivos.
  • headers (um objeto de no máximo 32 nomes de cabeçalho com valores de string de no máximo 4.096 caracteres, sem quebras de linha). Enviado à URL solicitada, seus saltos de redirecionamento de mesma origem e, na faixa do navegador, os arquivos que a página carrega dessa origem, após a identidade declarada do Octocrawl, que eles nunca podem substituir: User-Agent, qualquer cabeçalho sec-ch-* ou sec-fetch-*, as credenciais Authorization, Proxy-Authorization e Cookie, e os cabeçalhos de transporte (Host, Accept-Encoding, Connection, Content-Length, ...) são recusados com HTTP 400 nomeando-os (headers.user-agent is refused: the User-Agent and client hints are Octocrawl's declared identity; headers.cookie is refused: credentials are not sent as headers; mode 'authed' carries your own session on the record; headers.accept-encoding is refused: transport headers are set by the lane). Nomes são convertidos para minúsculas e um nome dado duas vezes é recusado; cabeçalhos Accept, Accept-Language, Referer, Cache-Control, If-None-Match e X-* são os usos comuns. Um redirecionamento para outra origem é buscado apenas com a identidade em ambas as faixas e o rastreamento diz isso (custom_headers_withheld com a URL e os nomes); na faixa do navegador, os cabeçalhos são adicionados por solicitação através da interceptação de solicitações do próprio Chromium, que julga cada salto e cada arquivo que a página carrega pela sua origem, então uma navegação que a própria página faz para outra origem (um script, um meta refresh) e um arquivo de mesma origem que redireciona para outro lugar também não recebem nenhum. robots.txt é sempre buscado apenas com a identidade. Tudo em headers está no registro: o rastreamento (request_headers_added, valores incluídos), a lista identity_sent da faixa HTTP e a conformidade assinada sentHeaders da faixa do navegador (que nunca carregam um cabeçalho Cookie ou Authorization: uma sessão está no registro como um hash), então segredos pertencem ao caminho da sessão autenticada, não a cabeçalhos. Na faixa do navegador, o local do contexto permanece en-US, então um Accept-Language que você definiu pode diferir do navigator.language da página; isso é visível para a página, não oculto. Uma solicitação com headers nunca avança para uma faixa de fornecedor (ladder_channels_filtered na auditoria da escada nomeia as faixas descartadas).
  • mobile (padrão false). true busca como a segunda identidade de navegador declarada do Octocrawl: Android Chrome (Mozilla/5.0 (Linux; Android 14; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/<major>.0.0.0 Mobile Safari/537.36, o mesmo Chrome major da identidade de desktop), dicas de cliente alinhadas (sec-ch-ua-mobile: ?1, sec-ch-ua-platform: "Android"), um viewport de 412x915 a 2.625 pixels de dispositivo por pixel CSS, e toque. Passa pelas mesmas verificações de coerência e honestidade que a identidade de desktop, robots.txt é avaliado contra seu User-Agent, e o rastreamento o registra (identity_sent com device: "mobile" na faixa HTTP, identity_declared na faixa do navegador); o registro de conformidade da faixa do navegador carrega o User-Agent móvel e as dicas em seu sentHeaders como enviado, e não tem campo de dispositivo (schemaVersion 2; adicionar um seria schemaVersion 3 e um novo hash para cada registro, uma decisão do proprietário). Na faixa do navegador, a identidade também é definida como metadados de user-agent do próprio Chromium, então as dicas de cliente que o Chromium gera sozinho (em um salto de redirecionamento, nas solicitações próprias da página) e navigator.userAgentData carregam as mesmas marcas, plataforma e sinalizador móvel que os cabeçalhos, para a identidade de desktop tanto quanto para a móvel. A página é o que o site serve a essa identidade: um redirecionamento para seu host móvel, um layout responsivo a 412 pixels CSS, ou uma página sem meta viewport disposta a 980 pixels CSS e escalada, como em um telefone; o Octocrawl não reescreve nada. A identidade móvel afirma Android no Chromium de desktop da mesma forma que a identidade de desktop afirma macOS em qualquer host: internamente coerente, não uma declaração sobre a máquina. É recusada com mode: "research" (mobile is not available in research mode: the research identity declares a bot, not a device) e nunca enviada a uma faixa de fornecedor. O Registro de Evidências diz qual identidade respondeu: identity.device é mobile ou desktop como a faixa respondente declarou (nulo no modo de pesquisa, que não declara dispositivo, e quando nenhuma solicitação foi enviada), e identity.requestHeaders lista os headers que essa faixa enviou, nomes em minúsculas e ordenados, cada um com o SHA-256 do seu valor (valueSha256), nunca o valor ([] quando nenhum).
  • skipTlsVerification (padrão false). Por padrão, um certificado que não é verificado (autoassinado, expirado, nome incorreto) é failed com failureReason: "tls_error" em ambos os degraus, com o código de erro no rastreamento (request_failed com reason: "tls_error" e code, como DEPTH_ZERO_SELF_SIGNED_CERT ou CERT_HAS_EXPIRED), também quando é a solicitação de robots.txt que falha nele, e a escada não escala por causa disso. true, em um servidor local, carrega a página mesmo assim: o degrau HTTP usa rotas próprias que não verificam, para esta busca e sua consulta de robots.txt, e as fecha depois (as rotas compartilhadas, as do cache de robots e as do worker de entrega continuam verificando); o degrau do navegador abre seu contexto com ignoreHTTPSErrors. Todo resultado de tal busca carrega um evento de rastreamento tls_verification_skipped e um aviso tls_unverified ("O certificado de não foi verificado a pedido do chamador; o conteúdo não pode ser atribuído a esse host com certeza."), após um aviso robots_overridden quando houver um, também em itens de lote e páginas de rastreamento; o registro de conformidade do degrau do navegador (schemaVersion 2) não tem campo TLS, então o rastreamento e o aviso são o registro. Um servidor hospedado (--hosted, o host MCP hospedado) recusa a opção com HTTP 400 skipTlsVerification is not available in hosted mode antes que qualquer coisa seja buscada, e executa uma tarefa armazenada sem ela. Nunca enviado a um degrau de fornecedor.
  • fastMode (padrão false). true mantém apenas o degrau HTTP: nenhum Chromium é iniciado, channelsTried é ["http"], summary.browserMs é 0, e a auditoria da escada registra os degraus que descartou (ladder_channels_filtered com reason: "fastMode"). O veredito do próprio degrau HTTP é a resposta: uma página cujo conteúdo seus scripts escrevem é failed/empty_unverified com a página como evidência, nunca renderizada, e um sucesso fino ou com aparência de renderização no cliente mantém seus avisos. Quando esse degrau pediu o degrau do navegador, a resposta (completa e compacta) carrega agentHints: ["the http lane asked for the browser lane; fastMode declined it; retry without fastMode"]. Ele pula o degrau do navegador; não torna uma página que o degrau HTTP já serve mais rápida, e waitFor não tem efeito sob ele. Uma URL que o servidor vincula à pista do navegador (o fluxo hospedado da Amazon) recusa com HTTP 400 fastMode is not available for this URL: it is served by the browser lane only.
  • blockAds (padrão true). Por padrão, o extrator remove contêineres de anúncios (elementos cujo id ou classe tem um dos tokens ad, ads, advert, advertisement, sponsored, promo) e banners de consentimento de cookies (ids com cookie, consent, gdpr, cmp, onetrust, didomi, usercentrics e similares, e elementos cujo aria-label nomeia cookies ou consentimento) antes da extração em cada degrau, e o degrau local do navegador aborta, antes de qualquer conexão, toda solicitação a uma lista embutida de cerca de cinquenta hosts revisados de exibição de anúncios (doubleclick.net, googlesyndication.com, adnxs.com, criteo.com, taboola.com, outbrain.com, amazon-adsystem.com, ...; packages/bench/src/subjects/adHosts.ts, correspondidos por host ou sufixo de ponto), registrando seu número e os primeiros 20 hosts em um evento de rastreamento ads_blocked. O degrau HTTP não busca recursos de página, então apenas o interruptor de extração se aplica lá. A lista de tokens é heurística: um elemento cuja classe por acaso é promo também é removido, e é por isso que o interruptor existe; a lista de hosts é curada, não EasyList, e não é baixada ou atualizada, então anúncios de hosts fora dela carregam (falsos negativos são esperados). false mantém anúncios e banners de cookies em markdown e html e carrega todo host; rawHtml é sempre a página como recebida. A lista de permissões de hosts do navegador hospedado e a regra de imagem, fonte e mídia permanecem em vigor independentemente do que blockAds diz, então false não pode ampliá-las.
  • removeBase64Images (padrão true). Por padrão, um <img> cujo src é um URI data: é deixado de fora do Markdown e seu texto alternativo mantido, em cada degrau e na página que um resultado falho mantém como evidência: Octocrawl sempre fez isso, que também é o que o removeBase64Images do Firecrawl faz por padrão (Octocrawl mantém o texto alternativo onde o Firecrawl escreve um espaço reservado (<Base64-Image-Removed>)). false mantém a imagem como ![alt](data:image/…;base64,…), e usage.contentTokens então a conta. html e rawHtml nunca são reescritos: eles são a página. Um link cujo destino é um URI data: é sempre escrito como seu texto. Uma escolha de renderização, não um fato de busca: nenhum evento de rastreamento, aviso ou mudança de conformidade; o serviço MCP hospedado recusa a opção como toda opção fora de sua lista de permissões. includeTags e excludeTags aceitam seletores de tag, classe, id e atributo, os combinadores de descendente e filho, :root, :empty e :not(), :is() e :where() ao redor de seletores sem combinadores, por exemplo table.wikitable, main > article p:not(.note) ou a[href$=".pdf"]. Um seletor que não faz parsing resulta em HTTP 400 invalid_request (includeTags entry is not a valid CSS selector: div[[). Um que faz parsing e usa qualquer outra coisa é recusado com unsupported_parameter, nomeando o que usa e seu lugar (excludeTags entry uses :nth-child, which Octocrawl does not match: li:nth-child(2), depois a lista suportada; details.parameters: ["excludeTags[1]"]): os combinadores irmãos + e ~, pseudo-classes posicionais como :nth-child e :first-child, :has(), :contains() e todas as outras pseudo-classes. Octocrawl não executa essas porque a biblioteca DOM que usa as corresponde a um custo que o tamanho da página não limita: em uma página de 200 parágrafos, x ~ p ~ p ~ p levou 4,7 s e mais um ~ p não terminou em 20 s, e o timeout de uma raspagem não pode parar um seletor enquanto ele está correspondendo. Os seletores que Octocrawl aceita são correspondidos em tempo proporcional ao tamanho da página, não importa quantos combinadores eles encadeiem, e uma lista é limitada a 100 partes de seletor no total para que a proporção também seja limitada: um nome de tag, *, uma classe, um id, um teste de atributo e uma pseudo-classe contam como um cada, aqueles dentro de :not(), :is() e :where() incluídos, então main > article p:not(.note) tem cinco; uma lista mais longa é HTTP 400 invalid_request nomeando a contagem (includeTags must hold at most 100 selector parts in all, and holds 102). Cada parte custa um teste de cada elemento e cada seletor composto uma passagem pela página: em uma página de 1,2 MB e 55.000 elementos, uma lista de 50 cadeias descendentes com 100 seletores compostos distintos levou 0,53 s, div:not() com 98 alternativas 0,06 s, e extração com tal lista em ambos includeTags e excludeTags 1,6 s contra 0,3 s sem eles. Seletores são lidos com o próprio parser da biblioteca DOM, então um nome escapado é o que é para a biblioteca: #\31 23, que CSS.escape escreve para o id 123, nomeia esse elemento, e .\32 xl\:grid a classe 2xl:grid.

As opções de cache. Octocrawl armazena o resultado bem-sucedido mais recente de cada página sob a raiz da tarefa (page-cache.sqlite) e o reutiliza apenas quando uma solicitação pede. Um resultado armazenado por página e conjunto de opções: a chave é a URL sem seu fragmento, o modo, toda opção que as pistas recebem exceto timeout (formats como html ou screenshot, onlyMainContent, includeTags, excludeTags, waitFor, headers, mobile, ...), os degraus que a solicitação pode usar (então a política de canal do servidor nunca é cruzada), fastMode, uma substituição de robots registrada e a build (as versões do extrator e W2L_SOURCE_COMMIT), então um resultado é reutilizado apenas para uma solicitação que o teria moldado da mesma forma, e seu Registro de Evidência nomeia a build que o produziu; após uma atualização, as páginas são buscadas novamente. Apenas um success com um tempo de busca registrado é armazenado, nunca uma página parcial, falha ou bloqueada; uma busca posterior da mesma página e opções o substitui, uma mais antiga nunca o faz. Uma solicitação com headers personalizado é armazenada apenas quando diz storeInCache: true, já que o rastro armazenado mantém os valores do cabeçalho. O cache não tem limite de tamanho ou expiração própria: exclua page-cache.sqlite para esvaziá-lo.

  • maxAge (milissegundos, 0 a 315 360 000 000, padrão 0). Reutilize um resultado armazenado buscado no máximo há esse tempo em vez de buscar a página. 0 não consulta nada: a página é buscada ao vivo, como sempre foi.
  • minAge (milissegundos, mesma faixa, no máximo maxAge). Reutilize apenas um resultado armazenado com pelo menos essa idade; sem maxAge, de qualquer idade a partir deste.
  • storeInCache (padrão true, exceto para uma solicitação com headers personalizado, que armazena apenas com true). false não armazena nada desta solicitação.
  • lockdown (padrão false). Responda apenas de um resultado armazenado, nunca busque: uma página sem um é failed com failureReason: "cache_miss" e uma entrada agentHints, e nada é solicitado, robots.txt incluído. maxAge e minAge ainda limitam a idade quando dados; lockdown com maxAge: 0 é HTTP 400. Uma raspagem em bloqueio não lê sitemap, então precisa de sitemap: "skip" (HTTP 400 caso contrário).

Um resultado reutilizado é o da busca original, inalterado: seu conteúdo, seu evidenceRecord (fetchedAt, hashes, decisão de robots.txt, identidade) e seu rastro, com um evento cache_hit adicionado no final. A resposta diz isso em metadata.cacheState: "hit" com metadata.cachedAt, o fetchedAt da busca reutilizada; channelsTried é [] e usage não conta nenhuma solicitação, tentativa, byte ou tempo de navegador. Uma solicitação que consultou uma página e não encontrou nada que se encaixe diz cacheState: "miss" e a busca (um evento cache_miss, e cache_stored quando o resultado foi armazenado). Uma solicitação que não consultou nada (sem maxAge acima de 0, sem minAge, sem lockdown) não carrega cacheState algum: nunca é relatada como erro. Itens de lote e páginas de raspagem carregam cacheState e cachedAt da mesma forma, e uma página reutilizada tem cached: true e conta no cachedPages do relatório de raspagem; espera pelo ritmo de seu host como qualquer página, mas deixa o robots.txt do host Crawl-delay como estava. Uma URL que o allowlistedDomains da solicitação recusa nunca é respondida do cache. O modo authed nem armazena nem reutiliza (uma página lida com sua sessão permanece sua): uma consulta é HTTP 400 lá. As capturas de um Monitor nem leem nem preenchem o cache. O useCached de uma raspagem é uma coisa diferente: um retomada reutiliza as páginas que essa raspagem já buscou (acima); maxAge alcança os resultados de qualquer solicitação no mesmo servidor. /fc mapeia as quatro opções sob os mesmos nomes (veja docs/firecrawl-shim.md). formats recebe markdown, links, json, html, rawHtml, images, tables e screenshot, uma entrada de { "type": "attributes", "selectors": [{ "selector", "attribute" }] } e uma entrada de { "type": "screenshot", "fullPage", "quality", "viewport" }, em scrape, batch e crawl. images é cada URL de imagem do documento inteiro como recebido, como links: img src e cada candidato a srcset (então uma variante 1.5x/2x ou 480w é uma entrada própria), <picture> <source srcset> candidatos, os atributos de carregamento preguiçoso data-src, data-srcset, data-lazy-src e data-original em img e source, video[poster], <link rel="image_src">, og:image (e suas formas :url / :secure_url) e twitter:image; cada um resolvido contra a base do documento (<base href> ou a URL final), somente http(s) absoluto, o fragmento removido, cada URL uma vez, em ordem de documento, com URIs data: deixadas de fora e contadas. Não há filtro de extensão de arquivo, então uma URL de CDN sem extensão permanece, e includeTags, excludeTags e onlyMainContent não estreitam a lista. No nível do navegador, a lista é lida do DOM renderizado, então uma imagem que um carregador preguiçoso já moveu de data-src para src aparece uma vez. O trace registra images_collected com count, srcsetCandidates, lazy e dataUrisDropped. attributes lê, para cada seletor (1 a 50 entradas, um seletor de 1 a 200 caracteres sob as mesmas regras e o mesmo limite de 100 partes que includeTags, um nome de atributo correspondente a ^[A-Za-z_][A-Za-z0-9_:.-]*$ de no máximo 100 caracteres), os valores do atributo nomeado em cada elemento que o seletor corresponde no documento como recebido, como escrito no HTML (não resolvido; links e images carregam as formas resolvidas), elementos sem o atributo ignorados, em ordem de documento, [] quando nada corresponde: [{ "selector", "attribute", "values" }] em ordem de solicitação, com um evento de trace attributes_extracted (selectors, counts). Um seletor que não faz parsing é HTTP 400 attributes selectors[i].selector is not a valid CSS selector: …, um que o Octocrawl não corresponde unsupported_parameter nomeando formats[j].selectors[i], e uma solicitação pode carregar uma entrada de atributos (formats must contain at most one attributes entry). Ambos são retornados somente quando solicitados, pelas respostas de scrape completas e compactas (cuja lista formats os nomeia), itens de batch, páginas de crawl e /fc (data.images, data.attributes), e somente para uma página lida como conteúdo: um arquivo, uma página falha ou bloqueada e um resultado sem HTML não carregam nenhum; uma página sem imagens dá images: []. summary.attempts e auditorias armazenadas nunca os repetem. O nível do fornecedor (provedor) também os carrega, lidos da página que o fornecedor retornou, como carrega html e rawHtml. html é o HTML limpo do qual o Markdown é escrito: a região de conteúdo principal; com onlyMainContent: false o <body> da página com seu cabeçalho, navegação e rodapé, sem scripts, estilos, controles de formulário, mídia incorporada e os elementos excludeTags; com includeTags um <body> segurando os elementos nomeados. É a marcação própria da página: alvos de links e imagens permanecem como a página os escreveu (o Markdown os resolve), e os marcadores de layout do nível do navegador, que moldam o Markdown, não estão nela. rawHtml é a página como o nível respondente a recebeu, scripts e tudo: o corpo da resposta no nível HTTP, o DOM renderizado em um nível de navegador. Seus bytes UTF-8 fazem hash para snapshot.rawBodySha256 (o rawSha256 do Registro de Evidências). Ambos são retornados somente quando solicitados, pelas respostas de scrape completas e compactas, itens de batch, páginas de crawl e /fc, e summary.attempts os repete somente com debug: true (auditorias de batch e crawl armazenadas nunca o fazem). Eles são null para um arquivo e para um resultado que não é success ou partial: uma página falha ou bloqueada mantém seu Markdown como evidência, não seu HTML, e a página duplicate de um crawl desiste de ambos com seu Markdown. O outputSha256 do Registro de Evidências cobre Markdown e JSON, não html. screenshot (a string, o screenshot@fullPage do Firecrawl v1, ou uma entrada { "type": "screenshot", "fullPage", "quality", "viewport" } por solicitação: formats must contain at most one screenshot entry) captura a página renderizada como uma imagem no nível do navegador local, que tal solicitação então seleciona sozinho: channelsTried é ["browser_local"], nenhuma tentativa HTTP é feita, a auditoria da escada nomeia os níveis descartados (ladder_channels_filtered, motivo screenshot), um servidor sem um nível de navegador recusa o formato com HTTP 400 screenshot requires the browser lane, which this deployment does not offer, e fastMode ao lado dele é recusado da mesma forma (which fastMode declines). A captura é feita após carregamento, estabilidade e waitFor e antes que o DOM seja lido, então a imagem e o Markdown mostram a mesma página, com o scale: "css" do Playwright: a imagem é dimensionada em pixels CSS, 1280x800 para o viewport de desktop declarado (o fator de escala de dispositivo declarado 2 é relatado, não embutido na imagem) ou o viewport solicitado (inteiros 320..1920 por 240..1080, dentro da tela declarada de 1920x1080; com mobile, dentro da declarada 412x915), que é um tamanho de janela e não uma mudança de identidade: o User-Agent, client hints, locale, fuso horário, tela e fator de escala permanecem como declarados, e o trace registra screenshot_viewport. fullPage: true captura a altura inteira do documento na largura do viewport sem rolar primeiro, então seções que uma página carrega ao rolar podem aparecer não carregadas (o Chromium que o Playwright 1.62.1 instala capturou fixtures de 100.000 px de altura inteiras, sem recorte). quality (1 a 100) dá um JPEG nessa qualidade, caso contrário um PNG. O screenshot da resposta é { contentType, width, height, fullPage, viewport, deviceScaleFactor, quality, bytes, sha256, path, base64 }: os bytes inline, com hash para sha256; path é <sha256>.png ou .jpg sob W2L_CAPTURE_RAW_DIR quando isso está definido (listado em snapshot.artifacts também, e no Registro de Evidências como kind: "screenshot" com seu tamanho e tipo), senão null; o trace registra screenshot_captured com o tamanho, os bytes, o hash e captureMs. A captura vai no que quer que a página renderizada acabe sendo, um sucesso, uma página de erro ou um portão mantido como evidência; é null quando o navegador não pôde tirá-la (screenshot_failed no trace, um aviso screenshot_unavailable e uma entrada agentHints, o resultado da página mantido) e quando nenhuma página renderizou (um arquivo, uma negação de robots.txt). summary.attempts e auditorias de batch e crawl armazenadas carregam screenshot: null, então a imagem viaja uma vez por resposta; um batch ou crawl com o formato pega cada página no nível do navegador e armazena cada captura inline em seu checkpoint, então capturas de viewport são a escolha mais leve lá. /fc a retorna como a string de data URI data.screenshot do Firecrawl. O nível do fornecedor (provedor) nunca a carrega: uma solicitação de screenshot mantém os níveis do navegador local sozinhos, então os níveis do fornecedor são descartados com o nível http (ladder_channels_filtered, motivo screenshot).

O formato list ({ "type": "list", "itemSelector": "article.product", "fields": [{ "name": "title", "selector": "h3 a" }, { "name": "url", "selector": "h3 a", "attribute": "href" }, { "name": "price", "selector": ".price" }] }, um por solicitação, 1 a 50 campos, nomes únicos) transforma uma página de itens repetidos em registros: cada elemento que itemSelector corresponde é um (um elemento dentro de outro correspondido é parte dele, não um registro próprio), e cada campo é lido dele: o texto de sua primeira correspondência dentro do registro como um leitor o vê (scripts e estilos deixados de fora, blocos mantidos separados, espaços em branco colapsados); o próprio elemento do registro quando o campo não tem selector; ou o atributo nomeado em vez do texto, um link ou fonte (href, src, data-src, ...) tornado absoluto contra a página, seu <base href> incluído. Nomes de campos são únicos, e source_url, page e index são as próprias colunas do CSV, então nenhum campo os toma. Os seletores seguem as regras includeTags e são verificados antes que qualquer coisa seja buscada. O list da resposta é { itemSelector, fields, records: [{ values, missing, source: { url, page, index } }], pages, incomplete, truncated, csv, csvSha256 }: um valor que o registro não tem é nulo e nomeado em seu missing, nunca preenchido ou adivinhado, e incomplete conta os registros com um; csv tem os campos, então source_url, page e index, então cada linha pode ser rastreada até a página e o lugar de onde veio. Lido da página como recebida (o DOM renderizado em um nível de navegador), sem modelo. Com uma ação paginate, os registros são aqueles de cada página que leu, cada um com seu número de página, também quando um passo posterior falhou; uma página cujos registros repetem uma página já mesclada não é contada novamente. Caso contrário, eles são aqueles da página como está. Uma lista para em 10.000 registros ou 5.000.000 de caracteres de valores: truncated é então verdadeiro e um aviso list_truncated diz isso. Uma página com pelo menos um registro segurando um valor não é falha por não ter conteúdo principal (uma lista de produtos ou citações não é um artigo): ela responde success, seu Markdown a página inteira; elementos que correspondem mas não seguram nada (um esqueleto de carregamento) não contam. A página duplicada de um crawl não carrega registros. Itens de batch carregam o list de sua página. /fc não o oferece.

{ "type": "list" } sozinho encontra a lista da página em si, e { "type": "list", "itemSelector": "..." } os campos dos itens nomeados (fields sem itemSelector é recusado). A lista são os elementos que se repetem lado a lado com a mesma tag e classes sob pais da mesma tag e classes (as linhas de uma grade juntas), pontuados por quantos são, quanto texto seguram e quão semelhantes são por dentro. Uma lista em nav, header, footer ou aside, sob um papel de menu, ou oculta (hidden, aria-hidden, um <details> fechado) nunca é escolhida e nenhum de seus itens é lido como um registro (itens ocultos por seu próprio atributo hidden ou aria-hidden, como esqueletos de carregamento, são mantidos fora com :not([hidden]):not([aria-hidden="true"]) no itemSelector); um menu por sua classe (menu, dropdown, tabs, pagination), ou itens que são cada um um link curto, contam pouco. Seus campos são o que pelo menos 60% dos itens seguram no mesmo lugar dentro deles: o texto de um elemento, o texto de um link e href, o src de uma imagem; um texto que todo item tem o mesmo (um rótulo, um botão) não é um campo. Cada campo é nomeado após sua classe (price para uma soma de dinheiro), nunca source_url, page ou index; um item que carece de um campo o tem ausente, nunca o valor de outro elemento (o seletor de cada campo é verificado em cada item, e um que o trabalho limitado não pode verificar é deixado de fora). Os seletores escolhidos permanecem dentro do que uma solicitação pode enviar (200 caracteres cada, 100 partes de seletor no total), e o trabalho é limitado em qualquer página. O itemSelector da resposta é o escolhido e list.detected é { fields, alternatives: [{ itemSelector, count }] }: os campos como uma solicitação os nomeia, para enviar de volta como estão ou editados, e as outras listas encontradas, melhores primeiro. Uma página sem lista responde itemSelector: null, sem registros e um aviso list_not_detected. Com paginate, a lista é encontrada na primeira página e cada página é lida da mesma forma. É um palpite da estrutura da página, sem modelo: verifique detected antes de confiar nele. tables fornece cada tabela de dados do conteúdo a partir do qual o Markdown foi escrito (o conteúdo principal, a página inteira com onlyMainContent: false, ou a seleção de includeTags) como dados: uma entrada por tabela GFM desse Markdown, em sua ordem, então tableIndex N é a enésima tabela GFM. Tabelas de layout e tabelas de linha única não são tabelas de dados, e uma tabela aninhada em uma célula é o texto dessa célula, como no Markdown. Cada entrada é { tableIndex, caption, sourceUrl, headerRows, columns, rows, csv, csvSha256 }: caption é o <caption> como texto simples (null quando não há; um título escrito acima da tabela não é uma legenda), sourceUrl a URL final da página, headerRows as linhas iniciais em <thead> ou feitas apenas de células <th>, e rows as células como texto simples (um link é seu texto, uma imagem seu texto alternativo, espaços em branco colapsados, nada escapado) com uma célula que abrange linhas ou colunas repetida em cada espaço que cobre, então cada linha tem columns células e nenhuma é deslocada. csv são as linhas como CSV RFC 4180 (fins de linha CRLF, um campo citado quando contém uma vírgula, uma aspa ou uma quebra de linha, aspas duplicadas) e csvSha256 seu SHA-256. Os spans são lidos e limitados como os navegadores os leem: os dígitos iniciais do atributo (2.5 é 2), 1 quando não há nenhum, é negativo ou é um colspan de 0, um rowspan de 0 até o fim de seu <thead>, <tbody> ou <tfoot> (ou da sequência de linhas diretamente na tabela), no máximo colspan 1.000 e rowspan 65.534. Um rowspan cobre cada linha que abrange, também uma cujas células terminam antes de sua coluna, e nunca passa do fim de seu grupo de linhas. As linhas estão na ordem em que os navegadores as organizam: o primeiro <thead> primeiro e o primeiro <tfoot> por último (um vazio incluído), onde quer que estejam escritos; um <thead> ou <tfoot> posterior permanece onde está. Uma página é analisada pela construção de árvore do padrão HTML (parse5), então uma tabela contém as linhas e células que um navegador constrói a partir das mesmas tags, incluindo as mal aninhadas: uma linha ou grupo de linhas escrito dentro de uma célula fecha a célula, texto escrito entre linhas vem antes da tabela, uma tag de fim que um navegador ignora não fecha nada, e um <tr> ou <td> próprio de um svg não é uma linha ou célula. Um fragmento (como o conteúdo principal) é lido como o conteúdo de um <template>, então uma linha ou célula de uma tabela permanece uma. Uma tabela cujas células, spans repetidos e cada linha preenchida até a mais larga, excederia 2.000.000 de caracteres, ou o que resta de 5.000.000 para as tabelas da página juntas (uma célula conta seu texto e seu escape CSV e JSON), é fornecida como { tableIndex, …, rows: [], csv: "", omitted: "too_large" }, então uma página pequena não pode criar um CSV enorme. O trace registra tables_extracted com a contagem, as linhas e colunas de cada tabela, e os índices das tabelas omitidas. Retornado apenas quando solicitado, pelas respostas de scrape completas e compactas, itens de lote e páginas de crawl, e apenas para uma página lida como conteúdo ([] para uma página sem tabelas); /fc recusa o formato, que o Firecrawl não possui. As tabelas de um PDF não são reconstruídas (veja texto do PDF).

Um scrape ou um lote também aceita actions: etapas que o navegador local executa na página após o carregamento, estabilidade e waitFor, e antes do formato de captura de tela e do conteúdo serem lidos, então uma página cujos dados aparecem apenas após uma interação pode ser lida (um botão "carregar mais", um formulário de busca, uma lista infinita, uma aba). As etapas são as do Firecrawl: wait (milliseconds até 60.000, ou um selector aguardado por até 60 s), click (selector; all: true clica em cada correspondência; um controle que outra coisa continua cobrindo, um modal ou um banner de consentimento, falha a etapa em cerca de 10 s, nomeando o que o cobre, uma vez que permaneceu coberto durante uma tentativa inteira de 5 s do clique, assim como os cliques de loadMore e paginate; uma cobertura que desaparece mais cedo é aguardada), write (text, digitado no elemento que tem foco, então clique nele primeiro), press (key: Enter, Tab, ArrowDown, ...), scroll (direction up ou down, uma tela da página ou do elemento que um selector nomeia), screenshot (fullPage, quality, viewport), scrape (o HTML da página naquele ponto), executeJavascript (script, um corpo de função cujo valor return é mantido; ele executa através do protocolo DevTools, então a Política de Segurança de Conteúdo da página não o impede) e pdf (format A0 a A6, Carta, Ofício, Tabloide ou Ledger, landscape, scale 0,1 a 2); no máximo 50, verificados antes que qualquer coisa seja buscada. Eles executam em ordem, cada um registrado no trace como um evento action com seu índice, tipo, resultado e tempo (e navigatedTo quando moveu a página). O actions da resposta contém o que eles produziram: screenshots (como evidência do formato de captura de tela), scrapes ({ url, html }), javascriptReturns ({ type, value }) e pdfs ({ format, landscape, scale, bytes, sha256, base64 }), cada um na ordem das etapas. Uma etapa que falha interrompe as etapas depois dela: actions.failed a nomeia (index, type, code, message; os códigos são selector_not_found, selector_timeout, script_error, navigation_refused, deadline_exceeded e action_error), e uma página lida como conteúdo é failed com action_failed, seu Markdown a página como estava, já que não é a página que as etapas deveriam alcançar. Cada etapa é limitada pelo timeout do scrape menos o segundo mantido para ler a página: uma etapa que não termina a tempo falha como deadline_exceeded, e o que as etapas anteriores produziram é mantido. Desde a primeira etapa até a página ser lida, uma navegação da página (um clique em um link, um redirecionamento de script, um formulário) passa pelo robots.txt e pela política de egresso como a URL solicitada fez, antes de sua solicitação ser enviada: uma que o Octocrawl não busca é respondida 204 No Content dentro do navegador, então nunca é solicitada e a página permanece onde estava, com um evento de trace navigation_refused e a etapa que levou a ela falhando como navigation_refused (uma página que as etapas deixaram em tal URL através de um redirecionamento de servidor não é lida). Uma página que a navegação de uma etapa alcança através de um redirecionamento de servidor é verificada quando carrega, e uma etapa que cai em uma que o Octocrawl não busca falha ali, não mantendo nada que leu dela; um redirecionamento da própria URL solicitada é da busca, não de uma etapa, e a URL solicitada, quando o robots.txt foi deixado de lado para ela (uma URL que a solicitação nomeou em um servidor local, ou um robotsOverride), é buscada como a solicitação disse; qualquer outra URL para a qual uma etapa navega é verificada contra o robots.txt. Uma janela que uma etapa abre é protegida da mesma forma e fechada imediatamente (popup_closed no trace). Uma mudança da URL dentro da página (history.pushState) não solicita nada e não é uma navegação. Uma URL que responde com um arquivo não tem página: suas etapas são relatadas como não executadas (action_error na primeira), nunca puladas silenciosamente. O pageActions do Registro de Evidências lista as etapas que executaram com seu resultado e diz se um script executou (scriptRan): os hashes são da página como as etapas a deixaram, então uma página que um script reescreveu está no registro como tal. Uma solicitação com actions seleciona apenas os degraus do navegador local (ladder_channels_filtered nomeia os outros), é recusada com fastMode e com as opções de cache (uma página após ações nunca é armazenada ou reutilizada), e é recusada por um servidor hospedado e pelo endpoint MCP hospedado; um crawl e um mapa não aceitam nenhuma. /fc mapeia o actions de um scrape (veja docs/firecrawl-shim.md).

Mais três etapas são próprias do Octocrawl, para listas que terminam apenas quando a página diz isso; cada uma para sozinha, nunca faz loop em um controle que permanece, e diz por que parou:

  • scrollToEnd (selector para rolar um elemento em vez da página, itemSelector para contar itens, maxScrolls 1 a 200, padrão 50, waitMs 100 a 10.000, padrão 1.000): rola até o fim, espera, e novamente, até que duas rodadas seguidas não adicionem nem altura nem itens (end).
  • loadMore (selector do controle "carregar mais", itemSelector, maxClicks 1 a 200, padrão 50, waitMs): clica nele, espera, e novamente, até que desapareça, fique oculto ou desabilitado (o atributo, aria-disabled, ou uma classe disabled nele ou ao redor dele) (end), ou dois cliques seguidos não adicionem nada (no_growth); um controle que nunca esteve lá é selector_not_found. Um controle oculto ou desabilitado antes do primeiro clique, ou enquanto os itens que ele solicitou carregam, é aguardado por até 10 s antes de encerrar a lista: muitas páginas mostram o seu apenas quando seu script executa.
  • paginate (nextSelector, itemSelector, maxPages 1 a 100, padrão 10, waitMs): lê a página, mantém seu HTML em actions.scrapes, segue Próximo e lê essa página, até que Próximo desapareça, fique oculto ou desabilitado (end) ou a página em uma URL mostre o que mostrou antes (repeat: Próximo levou de volta ou não fez nada). Uma página é lida duas vezes com 300 ms de intervalo e apenas o que lê o mesmo conta, então um relógio ou um preço em mudança não a faz parecer nova. Com itemSelector, os itens são os registros (cada um seus links e fontes de imagem e suas palavras estáveis): uma página cujos registros já foram lidos sob outra URL (uma primeira página em ambos /list e /list?page=1) é pulada, e duas dessas páginas seguidas (um site que responde cada página após a última com a última) encerram a lista. Sem itemSelector nenhuma página é pulada, então tal site é lido até maxPages, com o aviso abaixo. Cada página que alcança passa pelas verificações de navegação antes de ser lida; uma que o Octocrawl não busca falha a etapa como navigation_refused, não mantendo nenhuma de suas páginas. Um clique que nunca aterrissa falha a etapa como action_error, e as páginas lidas antes dele mantêm seus registros. Em um lote, cada página que a etapa lê é mantida no checkpoint da tarefa no momento em que é lida: um lote cortado na página N (desligamento, falha) retoma ali na próxima inicialização, passando pelas páginas mantidas ao longo dos próprios links Próximo do site e lendo o resto uma vez; actions.lists[].resumed diz quantas páginas vieram do checkpoint. Uma página mantida é conhecida novamente por seu endereço (quando as páginas têm endereços próprios), por seus itens, ou apenas pelos links de seus itens quando seu texto mudou entretanto e esses links diferem de página para página; uma página mantida conhecida por nenhum desses (linhas sem links cujo texto mudou) é lida novamente, seus registros se repetem, e maxPages a conta duas vezes. Uma verificação que o site coloca onde a próxima página deveria estar interrompe a etapa ali como challenge: um intersticial (Cloudflare, um pressione-e-segure do PerimeterX, um formulário de verificação) por suas próprias marcas antes da página ser lida, uma página de nada além de um widget CAPTCHA pelo veredito da página após as etapas (uma página com registros nela, ou com outro conteúdo, é uma página qualquer que widget carregue). As páginas antes dele mantêm seus registros, o resultado é blocked com o motivo da verificação, actions.lists[].challenge nomeia a página, e em um lote as páginas antes dele permanecem no checkpoint da tarefa; a transferência do lote então permite que você passe pela verificação e continue em seu próprio Chrome (veja a seção de transferência). actions.lists guarda, por etapa de lista, { index, type, stoppedBy, rounds, items } (items os elementos itemSelector correspondências no final, nulo sem uma; itemsRead para paginate, em todas as páginas). Uma etapa de lista que parou em seu limite (max), porque o prazo chegou (deadline) ou em uma verificação que o site apresentou (challenge, com challenge: { page, url, reason, signals }) não é a lista completa, e o resultado carrega um aviso de list_not_exhausted que diz isso. O Markdown é a página como as etapas a deixaram (para paginate, a última página); extrair registros de cada página é uma etapa posterior.

Um scrape também aceita robotsOverride ({ reason, recordedBy? }; reason de 1 a 500 caracteres, recordedBy de 1 a 200) na REST, no SDK e no MCP: sua própria razão registrada para buscar esta URL específica embora o robots.txt do host a desautorize ou não possa ser lido, por exemplo, um relatório que o editor vincula de suas próprias páginas em um host de arquivos cujas regras se dirigem a rastreadores. Um servidor local busca uma URL que um scrape nomeia mesmo assim (veja robots.txt acima); a substituição coloca sua razão e nome no registro no lugar de user_named_url. O robots.txt ainda é lido e seu veredito registrado. Quando ele desautoriza a URL, a busca prossegue e diz isso em todos os lugares: o trace carrega robots_disallowed e depois robots_overridden (url, appliedRules, reason, recordedBy), os warnings do resultado começam com { code: "robots_overridden", message } nomeando a URL do robots.txt, a regra, a razão e quem a registrou (respostas de scrape completas e compactas, itens de lote), o registro de conformidade da via do navegador mantém a desautorização com skippedFetch: false e um override que seu hash cobre, e o robotsDecision do Evidence Record permanece disallowed com userOverride: true e overrideBasis: "robots_override". Todo resultado de tal scrape carrega o aviso e esses eventos, qualquer que seja o degrau ou prazo que o produziu: quando o prazo passa enquanto a solicitação está fora (failed/timeout), ou quando um degrau posterior que não buscou nada responde (no modo authed, o salto do degrau authed_session quando não há sessão), o Octocrawl os adiciona a esse resultado com o evento robots_checked da via, cada evento nomeando a via que deixou a regra de lado, e o robotsDecision desse resultado é lido deles. A exceção é um erro do próprio Octocrawl após a regra ter sido deixada de lado: o scrape responde HTTP 500 internal_error, e um item de lote que falha com internal_error registra apenas esse erro. A substituição é para buscas de sua própria máquina: os degraus HTTP e navegador local a aplicam, a via provedora não aceita nenhuma, e um scrape que deixou uma regra de lado não prossegue para um degrau de fornecedor (ladder_channel_skipped na auditoria da escada), então nenhuma sessão de fornecedor é aberta para essa URL. Quando o robots.txt permite a URL, a substituição não faz nada e não deixa rastro. Um robots.txt inacessível também é deixado de lado, sua razão mantida no trace e no aviso. Um lote aceita robotsOverrides, uma lista de { url, reason, recordedBy? } na qual cada url é um dos urls do lote, nomeado uma vez; apenas essa URL é buscada além de sua regra, as outras URLs do lote não são afetadas, e a lista é armazenada com a tarefa, então um lote retomado a mantém. Um crawl e um mapa não aceitam nenhuma (eles aceitam ignoreRobotsTxt), e também não aceitam /fc, o endpoint MCP hospedado e a pré-visualização pública, que aceitam apenas suas próprias opções. Um servidor hospedado (npm run api -- --hosted) também não aceita nenhuma, nem ignoreRobotsTxt: quem quer que detenha um de seus tokens, esses campos são recusados com HTTP 400 unsupported_parameter nomeando o campo antes que qualquer coisa seja buscada, e um lote ou crawl armazenado que ele retoma é executado sem suas substituições. Em um scrape ou lote, ignoreRobotsTxt é recusado com HTTP 400 unsupported_parameter que o nomeia, assim como qualquer outra chave dentro de uma substituição.

Scrape, lote e crawl também aceitam dois rótulos que vão para os registros do próprio Octocrawl e em nenhum outro lugar: integration, o seu ("nightly-prices"), e origin, o do cliente. O SDK envia origin: "js-sdk@<version>" (seu SDK_VERSION, fixado à versão de seu pacote) com cada scrape, crawl e lote, a menos que a chamada defina um; o servidor MCP registra mcp-<client name>@<client version> do initialize do cliente (caracteres fora do ASCII imprimível escritos como _, cortados em 100; mcp@0.3.0 para um cliente que não declarou nenhum) e recusa um origin que uma chamada de ferramenta nomeia, então as ferramentas expõem integration sozinho; /fc mapeia o origin que os SDKs do Firecrawl enviam e também aceita integration. Cada um tem de 1 a 100 caracteres ASCII imprimíveis sem espaços, caso contrário HTTP 400 integration must be a string of 1 to 100 printable characters without spaces (o mesmo para origin). Nada enviado ao alvo muda, e nenhuma resposta os ecoa: o registro de um scrape carrega ambos (abaixo), e um lote ou crawl os armazena com sua tarefa, que GET /v1/batches/:id e GET /v1/crawl/:id relatam como attribution: { origin?, integration? } (ausente quando a solicitação não nomeou nenhum).

Um servidor pode comprimir sua resposta embora o Octocrawl não tenha pedido compressão: a identidade do Octocrawl não envia Accept-Encoding, e alguns sites (www.python.org) respondem com gzip mesmo assim. A via HTTP decodifica o corpo por seu Content-Encoding, pedido ou não: gzip (e x-gzip), deflate (com envoltório zlib ou cru) e br, uma lista de codificações em ordem reversa, após o limite de 10 MiB no fio (o limite de arquivo para um arquivo) e sob o limite de 50 MiB descomprimido (failed com decompressed_too_large além dele). usage.bytesWire é o tamanho recebido e bytesDecompressed o tamanho decodificado; rawSha256 e os bytes salvos de um arquivo são do corpo decodificado; evidence.contentEncoding e o contentEncoding do Evidence Record nomeiam as codificações (identity quando não houve nenhuma; nulo nas vias do navegador e provedora, que não o relatam). Qualquer outra codificação é failed com unsupported_content_encoding, e bytes que não decodificam conforme sua codificação são failed com parse_error (trace content_decoding_failed): nenhum é lido como a página. O leitor de sitemaps do crawl decodifica um arquivo de sitemap da mesma forma antes de inflar um arquivo .gz.

Um resultado também diz quando a página que leu parece um invólucro para dados que seus scripts preenchem. O extrator lê a página como recebida para um <table> sem células além de scripts, uma raiz de aplicativo (#root, #app, #__next e seus semelhantes) com quase nenhum texto, uma página de scripts com pouco texto, um elemento que a página oculta quando os scripts são executados (uma classe hide-if-js-enabled e seus semelhantes), um blob de hidratação (__NEXT_DATA__, window.__STATE__ =, window.__data = e afins) em uma página fina, aria-busy, uma mensagem de carregamento na região extraída ("Loading...", "Fetching quotes…", "Please wait": o texto inteiro de um elemento, com sua reticência, não de um botão ou link, nem dentro de hidden, aria-hidden="true" ou uma classe somente para leitor de tela) em uma página de no máximo 4.000 caracteres visíveis, ou, em uma página roteada como listagem ou coleção, uma lista em seu JSON de hidratação de pelo menos 10 registros nomeados distintos (name ou title, 12 caracteres ou mais) dos quais o texto visível mostra pelo menos 3 e no máximo metade (uma página de categoria do Walmart mostra 9 dos 49 produtos que seu __NEXT_DATA__ lista); cada regra emparelha uma lacuna estrutural com presença de script, então uma página estática com uma tabela vazia nunca aciona uma, e um aviso noscript "enable JavaScript" sozinho conta apenas em uma página fina (sob 1.500 caracteres visíveis) ou ao lado de estado de hidratação, já que sites estáticos carregam um ao lado de seus scripts de análise. Quando a página do degrau HTTP é lida dessa forma, seu status permanece o que o conteúdo mereceu, um success ou o resultado failed/empty_unverified que mantém a página inteira como evidência quando nenhuma região principal foi encontrada (uma página de moldura do site ao redor do script que escreve seu conteúdo é lida dessa forma), e o resultado carrega { code: "client_rendered_suspected", message } em warnings (após um aviso de robots_overridden quando houver um) nomeando a regra: empty_table_with_scripts, empty_app_root, script_shell, js_fallback, hydration_shell, aria_busy, loading_text ou hydration_list_partial. Seu trace carrega quality_client_rendered com a regra, os marcadores encontrados, o número de tabelas vazias e os tamanhos de texto visível e script (e, para hydration_list_partial, listRecords: os registros que a lista declara e quantos são mostrados), e a escada oferece a página à via do navegador como faz com um resultado fino (quality_low_yield): a página renderizada é a resposta quando contém mais, caso contrário a página HTTP permanece, aviso incluído, com esse salto marcado como improved: false (trigger: "quality_client_rendered"). Um invólucro failed/empty_unverified vai para a via do navegador em seu próprio pedido extract_low_confidence, e a evidência mantida quando essa via então falha sem uma página (ladder_evidence_kept) carrega o aviso. A via do navegador nunca o levanta: sua captura é a página renderizada. A leitura do próprio extrator é render em sua saída (@w2l/extract-tf): clientRendered, reason, markers, emptyTables, textChars, scriptChars e, com hydration_list_partial, listRecords. Quando tal resposta HTTP fina ou semelhante a invólucro permanece a resposta, ela também carrega { code: "low_content_yield", message }, após seus outros avisos: The http lane extracted N tokens at confidence C; the browser lane did not improve it. quando a via do navegador respondeu sem mais nada, ou falhou, e a página HTTP foi mantida (ladder_best_kept, o salto de qualidade improved: false), e …; the browser lane was not available to this request. quando nenhum degrau adicional foi permitido (fastMode, um servidor vinculado ao degrau HTTP, nenhuma via de navegador configurada); N e C são as próprias figuras quality_low_yield do degrau HTTP (senão sua confiança extract), e em um invólucro failed/empty_unverified a frase lê The http lane found no main content at confidence C; …. Uma página renderizada que respondeu não carrega tal aviso. Toda resposta que carrega warnings também carrega warning, suas mensagens unidas com um espaço, o nome do Firecrawl para isso: respostas de scrape completas e compactas, itens de lote, páginas de crawl e data.warning em /fc, que assim passa os avisos nativos adiante. Uma resposta também diz o que mudar na solicitação da próxima vez, em agentHints (respostas de raspagem completas e compactas, itens de lote, páginas de rastreamento e o registro de raspagem; agent_hints em páginas /fc), uma frase cada, presente apenas quando algo se aplica e nunca citando texto da página, apenas um host, um status, uma regra ou um horário: para um policy_denied com um evento robots_disallowed, robots.txt of <host> disallows this URL for Octocrawl's identity (rule <patterns>). A local Octocrawl server fetches a URL a scrape or batch names whatever robots.txt says, and a crawl or map started there with ignoreRobotsTxt fetches the links it disallows, each on the record; a hosted server obeys robots.txt for every URL (um robots.txt inacessível diz que conta como uma proibição completa e que o Octocrawl o solicita novamente após cinco minutos, então nomeia as mesmas rotas); para um login_wall, the page asks for a login; Octocrawl does not create accounts; use mode authed with your own session; para cloudflare_challenge, captcha e bot_detected_generic, <host> gates automated access on the lanes tried (<lanes>); Octocrawl does not solve challenges or change its identity; a proxy or session you own is the supported route, or, on your own machine, getting through the check yourself in your own Chrome: handoff: true on a scrape (octocrawl scrape --handoff), or a batch handoff (octocrawl batch --handoff, POST /v1/batches/:id/handoff); para um retryAt, wait until <ISO time> before asking <host> again (um bloco rate_limit sem Retry-After diz isso); para um corte, the content was cut at character <n>; ask for rawHtml or a narrower includeTags; para um aviso de client_rendered_suspected, the page fills its data with JavaScript; the browser lane was tried ou ... was not tried; para um aviso de low_content_yield, the http lane's content was thin and the browser lane did not improve it (ou was not available) ; pass waitFor (up to 60000 ms) or a longer timeout with the browser lane available, or actions (a click, a scroll, a wait for a selector) when the data appears after an interaction; sob fastMode, a única frase dessa opção fica sozinha quando o degrau HTTP pediu o degrau do navegador; para um http_error que manteve sua página, the server answered <status>; the markdown is that error page, not the requested page (um 404 adiciona ; check the link, e um 404 sem página diz the server answered 404; check the link); para um policy_denied que foi da política de saída em vez do robots.txt (um evento de rastreamento ssrf_denied ou governance_refusal), the egress policy refused <host> (<reason>) and nothing was fetched; Octocrawl reaches public addresses, and a local server the addresses its policy allowlists; para uma página para a qual a via HTTP obteve blocked ou um erro HTTP e a via local do navegador então serviu, the http lane got <status>/<reason> (HTTP <n>) from <host> and the local browser lane served the page; expect other pages of <host> to need the browser lane too (lido da tentativa HTTP do resumo da escada; o salto comum da escada após uma resposta HTTP fina ou vazia não carrega nenhum); para um tls_error, the certificate of <host> did not verify and Octocrawl keeps verification on; a local server takes skipTlsVerification for one request, recorded in the trace and a tls_unverified warning, and a hosted server refuses it; para um timeout, no lane answered within the request's deadline; raise timeout (up to 300000 ms); para um resultado partial, the result is partial: the deadline passed with this much of the page read; raise timeout (up to 300000 ms) for the rest; para empty_unverified sem uma casca ou ressalva de conteúdo fino, Octocrawl found no main content on the page; onlyMainContent: false returns the whole page's Markdown as content, and includeTags names the elements to read instead (um PDF sem camada de texto: the PDF has no text layer, and Octocrawl runs no OCR); para um json incompleto, json is incomplete: the required field(s) <paths> ... not found on the page; modelFallback fills what the page does not state when the server has W2L_EXTRACT_BASE_URL and W2L_EXTRACT_MODEL, e para um fallback de modelo que não foi executado, the json model fallback did not run: <reason>; para um arquivo, the response was a <kind> file kept at <path>; markdown is its text layer (um arquivo CSV, JSON ou texto: markdown is its text as received; um arquivo XLSX, XLS ou ZIP: it has no markdown; not saved no lugar do caminho quando o servidor não mantém arquivos). Um aviso de tls_unverified não adiciona nenhum, e nenhuma dica sugere furtividade ou mudança de identidade. As dicas vêm de uma tabela fixa indexada por código de aviso, status, evento de rastreamento, problema JSON e tipo de arquivo, nunca do texto da página, e no máximo cinco permanecem, na ordem da tabela. Uma opção recusada que o Octocrawl não oferece recebe uma dica também no corpo do erro (agentHints; agent_hints em /fc): stealth e proxy: "stealth" ou "enhanced" (Octocrawl does not offer a stealth mode or stealth proxies; a proxy or session you own (mode authed) is the supported route), ignoreRobotsTxt em uma raspagem ou lote (robots.txt is always read and recorded; on a local server a URL a scrape or batch names is fetched whatever it says, and ignoreRobotsTxt on a crawl or map fetches the links it disallows, on the record) e, em um servidor hospedado, skipTlsVerification (a hosted server verifies every certificate; run Octocrawl locally to use skipTlsVerification, which is recorded in the trace and a tls_unverified warning). O W2LError.agentHints do SDK os carrega (vazio quando não há nenhum).

O Markdown omite URIs data:, como o removeBase64Images do Firecrawl faz por padrão para imagens: uma imagem mantém seu texto alternativo e um link seu texto; removeBase64Images: false (acima) mantém o URI data: de uma imagem como seu alvo, o de um link nunca. Células de tabela e legendas mantêm seus links e imagens com alvos absolutos, como um parágrafo faz, em uma linha (um | em uma célula, alvo incluído, é escrito \|); ênfase e código em uma célula são texto simples. Uma tabela cujas tabelas aninhadas contêm pelo menos metade de seu texto, ou que tem uma única linha, organiza a página em vez de conter dados: suas células se tornam parágrafos, e apenas as tabelas de dados dentro dela se tornam tabelas GFM. Uma tabela de dados com uma tabela pequena em uma célula permanece uma tabela GFM, com a tabela pequena como texto dessa célula. Uma página que o Octocrawl extrai por suas tabelas (document.strategy: "table", para páginas cujas tabelas contêm pelo menos metade de seu texto) mantém como conteúdo principal o elemento mais baixo que contém todas as suas tabelas de dados, então os títulos, legendas e texto entre elas permanecem e o que está fora desse elemento, geralmente o cabeçalho, rodapé e coluna lateral da própria página, não; um menu organizado como tabela (principalmente links, e sem figuras fora deles) conta apenas quando é a maior tabela da página, e um <h1> solitário que compartilha um contêiner com as tabelas abaixo <body> é mantido com elas. Quando o degrau HTTP não encontra conteúdo principal e o degrau do navegador então falha sem uma página, a resposta é o resultado failed/empty_unverified do degrau HTTP com sua página (a auditoria da escada registra ladder_evidence_kept); quando o prazo encerra o degrau do navegador em vez disso, é failed/timeout com essa página, e o evento de rastreamento deadline_exceeded nomeia o degrau de onde veio (evidence).

Um resultado cuja página foi extraída carrega metadata, o que o HTML da página diz sobre si mesma, em respostas de raspagem (completas, compactas e MCP), itens de lote e páginas de rastreamento: title (seu <title>), description (<meta name="description">), language (<html lang>, ou <meta http-equiv="content-language"> quando <html> não tem lang), keywords e robots (esses valores <meta> como escritos), favicon (o primeiro <link rel="icon">, como URL http(s) absoluta resolvida contra a base do documento) e canonicalUrl (<link rel="canonical">, da mesma forma). Um valor que a página não declara é null; o Octocrawl não substitui og:description, um título ou /favicon.ico. Além desses sete, metadata carrega as tags Open Graph, Dublin Core e artigo que a página declara, sob os nomes do Firecrawl e apenas quando declaradas (ausentes caso contrário, nunca null): ogTitle, ogDescription, ogUrl, ogImage (og:image, senão og:image:secure_url, senão og:image:url), ogAudio, ogVideo (da mesma forma), ogDeterminer, ogLocale, ogLocaleAlternate (cada og:locale:alternate, como lista) e ogSiteName, lido de <meta property="og:…"> então <meta name="og:…">, a primeira tag não vazia vencendo, os campos de URL resolvidos contra a base do documento quando analisam; dcTermsCreated, dcDateCreated, dcDate, dcTermsType, dcType, dcTermsAudience, dcTermsSubject, dcSubject, dcDescription e dcTermsKeywords de <meta name="dcterms.…"> e <meta name="dc.…"> (nomes correspondidos sem diferenciar maiúsculas de minúsculas); e publishedTime (article:published_time), modifiedTime (article:modified_time), articleSection e articleTag (cada article:tag, como lista). Cada valor são as próprias palavras da página com espaços em branco colapsados: sem normalização de data, sem cálculo de fuso horário e sem fallback de tags twitter:*, govuk:*, citation_*, JSON-LD ou elementos <time>, então uma página que não declara nenhum não recebe nenhum. Um item de lote ou página de rastreamento falho ou bloqueado não tem metadata. Uma resposta de raspagem sempre tem um, porque também carrega os fatos da chamada (próximo parágrafo); em uma página que não foi lida como conteúdo (uma página de bloqueio, uma página de erro, um arquivo), seus campos de página são todos null: o Octocrawl não declara nada de uma página que é evidência em vez da página solicitada. metadata.title é o <title> da página, frequentemente com o nome do site adicionado; document.title é inalterado: o título do conteúdo, geralmente o primeiro título do conteúdo principal, com <title> apenas como seu fallback. /fc coloca title, description, language, keywords, robots e favicon em data.metadata quando a página os declara, e os campos Open Graph, Dublin Core e artigo sob os mesmos nomes, articleTag unido com , como o Firecrawl escreve e ogLocaleAlternate mantido como lista.

Toda resposta de raspagem (completa e compacta; /fc coloca as mesmas chaves em data.metadata, com creditsUsed: null) carrega os fatos da chamada sob os nomes do Firecrawl, ao lado dos campos da página: scrapeId (um UUID cunhado por chamada POST /v1/scrape ou /fc/v1/scrape; a resposta completa o repete no nível superior), sourceURL (a URL solicitada), url (a URL final, evidence.finalUrl), statusCode e contentType (da resposta que respondeu url, como em snapshot), proxyUsed ("operator" quando a solicitação passou pelo proxy de ambiente do servidor, lido de evidence.envProxy e dos eventos de rastreamento egress_proxy; "user" quando o registro de conformidade da via do navegador nomeia sua própria saída; null caso contrário, nunca um palpite), timezone (o fuso IANA que a via do navegador declara, America/Los_Angeles para a identidade de desktop e móvel igualmente; null para um resultado da via HTTP, onde nenhum fuso horário vai no fio), e concurrencyLimited com concurrencyQueueDurationMs (abaixo). cacheState e cachedAt não estão lá: o Octocrawl não tem cache ainda e não relata miss inventado. O registro da chamada, sem qualquer corpo de página, é escrito em <task root>/scrapes/<scrapeId>.json antes que a resposta seja enviada e servido por GET /v1/scrapes/:id (SDK getScrape(id), MCP get_scrape): scrapeId, requestedAt, request (as opções analisadas, o valor de cada cabeçalho personalizado substituído por seu nome), origin, integration, status, failureReason, blockReason, budgetExceeded, lane, channelsTried, metadata, snapshot, usage (wallMs, totalMs, requestCount, attemptCount, browserMs), warnings e agentHints. Um id desconhecido ou malformado é HTTP 404 not_found. Quando o registro não pode ser escrito, a resposta ainda carrega seu id, o servidor registra scrape_record_unwritten e o rastreamento da resposta completa registra esse evento. Registros são mantidos sem retenção: um operador hospedado poda o diretório. Uma captura de Monitor cunha um id para sua resposta, mas não escreve registro (uma prévia não persiste nada; a observação de uma execução é do próprio Monitor).

concurrencyLimited é true quando o teto de concorrência por origem (W2L_PER_HOST_CONCURRENCY, 1 a 4, por origem alvo) segurou uma tentativa da raspagem porque todos os slots estavam ocupados, e concurrencyQueueDurationMs é quanto tempo suas tentativas esperaram por um slot no total, um cooldown concorrente e o intervalo mínimo entre solicitações excluídos (cada via tentada adquire sua própria permissão, então uma raspagem que escalou adiciona as esperas de ambas as vias). Cada resultado de via carrega sua própria parcela como usage.timings.concurrencyWaitMs, presente apenas quando sua permissão foi segurada, em itens de lote e páginas de rastreamento também; usage.timings.queueMs mantém seu significado, toda espera de agendador exceto um cooldown, e inclui esse tempo. Diferente do concurrencyLimited do Firecrawl, que relata um limite por conta, o do Octocrawl é o portão de polidez por origem, e o agendamento de fronteira de um rastreamento (seu espaçamento por host) não é contado.

Solicite dados estruturados determinísticos com um JSON Schema junto, ou em vez de, Markdown:

const product = await w2l.scrape('https://www.amazon.com/dp/B08KT2Z93D', {
  debug: false,
  formats: [{
    type: 'json',
    schema: {
      type: 'object',
      properties: {
        asin: { type: 'string' },
        title: { type: 'string' },
        price: { type: ['number', 'null'] },
        currency: { type: ['string', 'null'] },
        seller: { type: ['string', 'null'] }
      },
      required: ['asin', 'title', 'price', 'currency', 'seller'],
      additionalProperties: false
    }
  }]
})

Octocrawl mapeia campos de produto suportados diretamente de evidências de HTML, JSON-LD, metadados e DOM vinculadas ao assunto. Uma chave de nível superior que nenhum desses fatos cobre é correspondida aos rótulos da própria página, linhas de tabela de duas células th/td e pares dt/dd no conteúdo principal, comparados sem diferenciar maiúsculas, espaços ou pontuação (Number of reviews preenche numberOfReviews; Price (excl. tax) preenche price quando nenhum rótulo é exatamente Price). title (ou pageTitle) assume o título do conteúdo, url (ou finalUrl) e requestUrl a URL final e solicitada da busca, e pageType o tipo de página que o Octocrawl classificou. Um número é lido apenas de texto que é um único valor, como £51.77 ou 1.299,00 €, nunca de texto como HL-1, 4.7 out of 5 ou uma URL (veja abaixo); rótulos que indicam valores diferentes deixam o campo de fora com um problema field_ambiguous. Um campo anulável ausente é null com um problema field_unavailable; um campo obrigatório ausente é deixado de fora com um problema missing_required e o resultado é incomplete. Cada valor tem uma entrada evidence (o mesmo mapa é evidenceRecord.fieldEvidence): a tabela ou lista do rótulo, linha e rótulo; dom com h1[0] (o primeiro título do conteúdo principal) ou title (o <title> da página) para o título; fetch com finalUrl ou requestedUrl para uma URL; inferred com document.pageType para o tipo de página, e com document.product.images (prices, variants, specifications) para uma lista ou mapa que o extrator de produto relatou como vazio, já que nada na página localiza uma ausência; model para um valor que o fallback do modelo escreveu. json.evidence também cita o texto do qual cada número foi lido (text, espaços em branco colapsados); o Registro de Evidências mantém source e locator.

Os números são lidos como a página os escreve. Um valor é um sinal opcional, um símbolo de moeda ou código antes ou depois do número (€, EUR, US$, kr, 円, ou um símbolo antes e um código depois, como em $12.99 USD) e um número: seu separador decimal é . ou ,, seus milhares são agrupados por ., ,, um espaço (espaços sem quebra e sem quebra estreitos também) ou um apóstrofo em grupos de três, ou nos grupos lakh da Índia, e ,- ou .– depois dele encerra um valor inteiro. Então 12,99 € é 12,99; 1.299,00 €, 1 299,00 €, CHF 1'299.– e $1,299.00 são 1299; ₹1,29,999 é 129999. Um único . ou , antes de exatamente três dígitos (1.299 €, $1,299) é 1299 em uma notação e 1.299 na outra, então é lido apenas quando o valor resolve isso: uma contagem de avaliações é inteira, assim como um valor em uma moeda sem unidades menores (JPY, KRW, ISK, VND, CLP, ₩, 円, no texto ou como o priceCurrency da página), e um preço JSON-LD ou product:price:amount escreve . como seu ponto decimal. Octocrawl não adivinha pelo idioma, moeda ou domínio da página: uma página em inglês de uma loja alemã pode escrever 1.299 €, lojas irlandesas escrevem €1,299, e uma página em alemão pode citar $1,299. Tal número, e um preço de produto que não é um número (Call for price), é deixado de fora, ou null quando o campo é anulável, com um problema field_unavailable citando o texto e onde está (um obrigatório também recebe missing_required); solicitado como string, o campo é o texto. Um valor na lista prices do adaptador Amazon que não pode ser lido permanece seu texto, com um problema field_unavailable em /prices/<i>/amount.

O esquema pode usar o subconjunto JSON Schema que o Octocrawl pode honrar, que cobre o que o model_json_schema() e o zod-to-json-schema do Pydantic geralmente escrevem:

  • Estrutura: type (um ou uma lista), properties, required, items (um esquema), additionalProperties, enum, const, $ref local (#, #/$defs/…, #/definitions/… ou outro ponteiro para o esquema) com $defs ou definitions, e anyOf / oneOf de um esquema e { "type": "null" } (o Optional do Pydantic) ou de tipos primitivos apenas.
  • Verificado no resultado, nunca usado para preencher um valor: minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf, minLength, maxLength, pattern, minItems, maxItems, uniqueItems, e enum / const. Um valor de página que quebra um permanece em data e o resultado é incomplete com um problema field_unavailable citando a verificação (o fallback do modelo, quando ativado, pode substituí-lo). Um pattern tem no máximo 2.000 caracteres, e um que pode retroceder catastroficamente, como ^(a+)+$, é recusado com invalid_request. O restante roda no mecanismo de tempo linear do V8, exceto um padrão com lookaround, uma referência anterior, uma repetição contada acima de 16 (como [0-9a-f]{32}), um escape \p{…} ou \u{…} ou um caractere fora do Plano Multilíngue Básico, e qualquer texto contendo tal caractere (um emoji): esses são correspondidos apenas contra texto de até 2.048 unidades de código UTF-16 e dentro de 100 ms, e texto que eles não conseguem decidir conta como quebra do padrão.
  • Aceitos e não aplicados: title, description, $comment, examples, deprecated, readOnly, writeOnly, format (não verificado), default (nunca preenchido: um campo que a página não fornece permanece ausente), e apenas na raiz $schema (draft-07, 2019-09 ou 2020-12) e $id.
  • Limites: 64 KiB, 8 níveis de aninhamento e 100 propriedades.

Qualquer outra coisa é recusada com HTTP 400 unsupported_parameter, cujo details.parameters nomeia a palavra-chave onde foi enviada (por exemplo formats[0].schema.properties.author.allOf): allOf, not, if, patternProperties, prefixItems, o nullable do OpenAPI, uma união de objetos, arrays ou referências, uma palavra-chave diferente de uma anotação ao lado de $ref, $schema ou $id abaixo da raiz. Um valor malformado, como um pattern inválido ou um $ref que não resolve, é invalid_request. Um formato json precisa de um esquema: { "type": "json", "prompt": "…" } sozinho é recusado com invalid_request, porque o Octocrawl não extrai JSON sem um (isso exigiria um modelo para cada página); prompt apenas instrui o fallback do modelo.

O fallback do modelo é opcional com modelFallback: true e roda apenas quando um campo obrigatório está ausente ou um valor quebra o esquema; configure um endpoint compatível com OpenAI através de W2L_EXTRACT_BASE_URL, W2L_EXTRACT_MODEL e W2L_EXTRACT_API_KEY opcional. Sem essas variáveis, o conteúdo da página nunca é enviado a um modelo e o resultado JSON relata model_unavailable. O modelo recebe o Markdown do conteúdo principal e os valores já lidos. Ele preenche apenas o que está ausente, ou substitui um valor de página que quebra o esquema; cada valor lido da página mantém seu valor e evidência independentemente do que o modelo responder, e cada valor que o modelo escreveu tem evidência model. A solicitação usa saídas estruturadas estritas (json_schema com strict: true) com uma cópia segura e estrita do esquema: cada objeto fechado, cada propriedade obrigatória e as opcionais anuláveis, asserções e anotações deixadas de fora. A resposta ainda é verificada contra seu esquema, com uma rodada de reparo, e um null que seu esquema não permite é descartado como "não encontrado". Um esquema que o modo estrito não pode expressar, como um objeto sem properties, é enviado como está sem modo estrito; json.modelUsage.strict diz qual foi usado e strictReason por que não.

Execute a linha de base fixa de 10 produtos, três rodadas do MCP Amazon com:

node scripts/section-b/amazon-public-state.mjs
npm run baseline:amazon -- --concurrency 1
npm run baseline:amazon -- --concurrency 2
# After 1 and 2 are comparable and unblocked:
npm run baseline:amazon -- --concurrency 4

A configuração usa uma preferência de entrega pública anônima de Singapura apenas para este benchmark. A rodada 1 fixa o contexto observado; registros posteriores não observados ou com região/moeda incompatíveis permanecem no relatório e não contam como comparáveis. Relatórios e HTML bruto permanecem sob .w2l/amazon-baseline/ ignorado; o manifesto de URL e o esquema são versionados. O resultado assinado de dez produtos passou com concorrência limitada, mas a Amazon permanece em beta até os portões de promoção 100/1000. A linha de base mais antiga é histórica. O comando de concorrência 1 pode sair com código diferente de zero porque sua mediana de dez páginas excede 20 segundos; inspecione seu relatório para comparabilidade e bloqueio antes de continuar para 2. A execução assinada teve 37,93 segundos em 1, 19,92 em 2 e 12,39 em 4. Esta fatia assinada da Amazon foi mesclada em main por PR #52, após o pré-lançamento da fonte v0.4.0-rc.1, então esse pré-lançamento não a contém.

Clientes Firecrawl v1 (compatibilidade parcial): defina a URL base para http://127.0.0.1:8787/fc para que /v1/scrape e /v1/crawl atinjam o shim. O shim de scrape mapeia url, os formatos markdown, links, html, rawHtml, images e screenshot (screenshot@fullPage e uma entrada { type: "screenshot", fullPage, quality, viewport } também, retornada como o data URI data.screenshot) e uma entrada { type: "attributes", selectors }, onlyMainContent, includeTags, excludeTags, waitFor, timeout, headers, mobile, skipTlsVerification, fastMode, blockAds, removeBase64Images as opções de cache maxAge, minAge, storeInCache e lockdown (um maxAge omitido não reutiliza nada; um scrape lockdown sem resultado armazenado é HTTP 404 SCRAPE_LOCKDOWN_CACHE_MISS), e proxy como a escolha de acesso (basic é standard; stealth e auto são enhanced, que um servidor sem uma concessão de acesso de nível aprimorado recusa com HTTP 400); o shim de crawl mapeia url, limit, maxDepth, includePaths, excludePaths, regexOnFullURL, ignoreQueryParameters, deduplicateSimilarURLs, crawlEntireDomain (e o allowBackwardLinks do v1), allowSubdomains, allowExternalLinks, sitemap (e o ignoreSitemap do v1: true é skip, false é include; sitemapOnly: true é only), maxConcurrency, webhook (no webhook de trabalho nativo, o receptor obtendo a forma de payload do Firecrawl) e o mesmo scrapeOptions. As cinco opções de execução seguem as regras do Octocrawl, não as do Firecrawl: um User-Agent ou Cookie em headers é HTTP 400, skipTlsVerification é recusado em um servidor hospedado, e fastMode retorna o veredito do nível HTTP em vez de um render. removeBase64Images (padrão true) mantém o texto alternativo de uma imagem onde o Firecrawl escreve um espaço reservado, e false mantém o URI data:. Qualquer outro parâmetro ou formato (location, json, um scrapeOptions.actions de crawl, ...) é rejeitado com HTTP 400 e success: false, nomeando-o. Snapshot 2026-09-18; diffs conhecidos em docs/firecrawl-shim.md. A compatibilidade de Firecrawl Search / Interact / Agent / Monitor não é implementada. As APIs nativas de Monitor e Delivery do Octocrawl usam seus próprios contratos.

Monitores Contínuos e entrega de eventos

O SDK nativo inclui paginação/cancelamento de Crawl, criação/revisões/execuções/controle de Monitor e destinos/status/repetição de Delivery. O exemplo executável usa uma fonte de preço controlada, captureMode explícito, linhas de base validadas, requisições HTTP condicionais e eventos persistidos. O receptor de webhook armazena recibos de eventos e aplica uma projeção de produto versionada transacionalmente.

A API, o agendador de Monitor e o worker de delivery compartilham um banco de dados de controle persistente. npm run local:mcp:install gerencia todos os três para usuários locais de MCP. w2l-api (npm run api) executa um worker de delivery próprio desde que os webhooks de job chegaram, então as entregas de Monitor e job saem do processo da API também; o worker autônomo abaixo ainda está lá para uma implantação que o queira separado, e executar ambos é seguro (uma entrega é alugada e cercada). Para uma implantação de API autônoma, execute o worker de Monitor em um terminal separado com o mesmo W2L_TASK_ROOT da API:

export W2L_TASK_ROOT="$PWD/.w2l/api"
npm run monitors:worker

O worker de Monitor usa por padrão a política de rede de fonte pública. Para a fonte local controlada no exemplo de integração, defina explicitamente W2L_MONITOR_NETWORK_MODE=local nesse terminal do worker. Um worker executado localmente não herda acesso de rede mais amplo da API ou do banco de dados.

export W2L_TASK_ROOT="$PWD/.w2l/api"
npm run delivery:worker

Veja integração para o receptor HTTPS, autenticação, configuração do worker e o exercício de reinício de entrega pendente. O congelamento de fonte do Gate 2–4 99894bd636ecafd254a7c7bc79d26e9a97fa9199 está em main através do PR #50 e é publicado como pré-lançamento de fonte v0.4.0-rc.1. Clone main ou faça checkout dessa tag. Os pacotes do workspace permanecem privados e a instalação humana independente continua pendente.

O registro de aceitação do Gate 2–4 vincula as evidências de falha de processo, reivindicação concorrente, HTTPS público e instalação limpa do agente. A aceitação de engenharia dos Gates 2/3 passou; o Gate 4 aguarda um humano não autor, e a validação externa de duas semanas/uso repetido do Gate 5 não começou. npm run package:handoff captura a fonte de revisão com hashes por arquivo. O arquivo testado existente é um snapshot pré-commit preservado, não um pacote das edições subsequentes do roteiro.

O MCP de Monitor/Delivery C2 e seu fluxo de trabalho local de primeiro uso via HTTPS estão implementados. O C3 tem um processo unificado e uma implementação autenticada de Streamable HTTP, experimental e não implantada (configuração arquivada). B1/B2 e C1 permanecem em_progresso para seus portões operacionais/de adoção mais amplos. Veja o passo a passo de primeiro uso e a evidência local datada.

Arquivos: PDF, CSV, XLSX, ZIP, JSON

Scrape, itens em lote, páginas de crawl, MCP e /fc aceitam uma URL que responde com um arquivo da mesma forma que uma página da web. Uma resposta 2xx é um arquivo quando seu Content-Type diz PDF, CSV (incluindo tipos +csv como o SDMX-CSV da Eurostat), JSON (e +json), texto simples, XLSX, XLS ou ZIP; quando não diz nada útil (application/octet-stream e similares, ou nenhum), os bytes decidem: um cabeçalho %PDF- nos primeiros 1024 bytes, um cabeçalho ZIP (um XLSX quando o arquivo é nomeado .xlsx), um cabeçalho OLE nomeado .xls, ou texto nomeado .csv, .json ou .txt. Um cabeçalho PDF no início substitui text/html ou text/plain, e um documento HTML enviado como text/plain ainda é uma página. O nome é o nome de arquivo Content-Disposition, caso contrário, o caminho da URL.

  • Salvo como recebido, nunca enviado ao navegador. O caminho HTTP salva cada byte em <W2L_TASK_ROOT>/files/<sha256>.<ext> (.w2l/api/files/ por padrão; npm run scrape usa o mesmo lugar, npm run crawl o diretório de tarefas files/), então os mesmos bytes são armazenados uma vez, não importa quantas URLs os sirvam. Um arquivo nunca é escalado para o navegador, qualquer que seja seu resultado. O bloco file do resultado dá o tipo, como foi detectado, o Content-Type como recebido, o tamanho declarado e recebido, o SHA-256, o caminho e, para um PDF, suas páginas; evidence.rawBodySha256 e o rawSha256 do Registro de Evidência são o SHA-256 dos bytes. Uma requisição recusada antes de qualquer coisa ser buscada (robots.txt, política, DNS) não salva nada, e o mesmo acontece com uma resposta não-2xx.
  • O caminho do navegador captura o download. Quando o navegador é o primeiro degrau (waitFor) ou de outra forma alcança um arquivo, ele pega o arquivo do download que a navegação inicia, ou da resposta que exibe (JSON, texto), em vez de falhar com Download is starting, e salva os mesmos bytes da mesma forma.
  • Limite de tamanho. W2L_MAX_FILE_BYTES define o maior arquivo em bytes (padrão 52 428 800, 50 MiB; no máximo 524 288 000, 500 MiB; qualquer outra coisa para o serviço na inicialização). Uma requisição, lote ou crawl pode reduzi-lo com maxFileBytes, nunca aumentá-lo (um valor maior é HTTP 400 invalid_request). Um arquivo acima do limite é failed com body_too_large, com seu tamanho declarado em file.declaredBytes quando o servidor enviou um; ele não é lido além disso e nada é salvo ou truncado. Páginas da web mantêm o limite de corpo de 10 MiB.
  • Outros tipos binários (imagens, áudio, vídeo, fontes, documentos Word e PowerPoint, outros arquivos) são failed com unsupported_content_type: não baixados, não salvos, não enviados ao navegador.
  • Um corpo que para ou se interrompe após os cabeçalhos é failed com timeout ou connection_error, sem nada salvo (foi um erro interno antes).

O que cada tipo retorna:

TipomarkdownStatus
PDFA camada de texto, uma linha <!-- page N --> antes de cada página (abaixo)success com texto; partial quando o limite de páginas (1000), o orçamento de tempo (60 s, ou menos quando o timeout do scrape está mais próximo) ou uma página ilegível o interromperam; failed/empty_unverified quando nenhuma página tem camada de texto (um scan: sem OCR); failed/parse_error quando não pode ser aberto (sem cabeçalho PDF, criptografado, malformado); failed/timeout quando não abriu a tempo
CSV, JSON, textoO texto como recebido, decodificado pela marca de ordem de byte, charset declarado ou UTF-8 (a marca descartada)success; sem texto e com um aviso text_not_decoded quando os bytes não são válidos nessa codificação
XLSX, XLS, ZIPnullsuccess; o arquivo é o entregável. Tabelas → análise de CSV e XLSX virão depois
Qualquer, sem bytesnullempty_verified

onlyMainContent, includeTags e excludeTags não se aplicam a arquivos, e waitFor não é aguardado uma vez que o arquivo chega. Um resultado de arquivo não tem document ou metadata e nenhum links, e seus html e rawHtml, quando solicitados, são null. O Registro de Evidência lista o arquivo em artifacts como { kind: "file", path, sha256, bytes, contentType } e nomeia o extrator pdf-text (PDF_TEXT_VERSION) para um PDF ou file-text (FILE_TEXT_VERSION) para outro arquivo; outputSha256.markdown cobre o Markdown entregue.

A extração de JSON lê um PDF deterministicamente: uma chave de esquema é correspondida, como em uma página da web, aos rótulos das linhas Label: value do PDF (por exemplo, KPI 2: Reduction of carbon intensity preenche kpi2), e fieldEvidence dá a cada campo { source: "pdf", locator: "page N \"label\"" }. Rótulos que declaram valores diferentes deixam o campo de fora com field_ambiguous; prosa e células de tabela não são lidas, os metadados do PDF não são usados, e modelFallback não é aplicado ao texto do PDF (um problema model_unavailable diz isso), então um campo não encontrado é relatado como ausente, nunca adivinhado. O texto do PDF é executado no thread do processo da API: o relatório da IEA de 304 páginas leva menos de um segundo.

Texto PDF

pdfToMarkdown(bytes, options?) em packages/extract-tf transforma os bytes de um PDF em Markdown com números de página, para que uma figura citada de um relatório possa ser rastreada até sua página. Scrape, lote, crawl, MCP e /fc o usam para cada PDF que buscam (veja Arquivos).

O que ele faz:

  • Lê a camada de texto do próprio PDF com Mozilla pdf.js (pdfjs-dist 6.3.289, Apache-2.0), em Node, sem renderização.
  • Inicia cada página com uma linha <!-- page N -->, sendo N a posição da página no arquivo, e retorna pages[]: o text de cada página, seu label impresso quando o PDF declara um, e os offsets start / end desse texto no Markdown. pdfPagesForSpan(pages, start, end) nomeia as páginas de onde qualquer trecho do Markdown veio.
  • Reconstrói linhas, espaços e parágrafos a partir das posições do texto, lê páginas de múltiplas colunas coluna por coluna e mantém linhas de tabela como linhas. Uma palavra hifenizada no final de uma linha é unida; o hífen é removido apenas onde o documento soletra a palavra sem ele em outro lugar.
  • Relata info como o PDF declara (título, autor, produtor, datas, idioma), nulo onde não declara nada.

O que ele não faz:

  • Sem OCR: uma página sem camada de texto (um scan) retorna vazia com um aviso no_text_layer.
  • Sem reconstrução de tabela: células se tornam linhas de texto, e todo resultado com texto carrega tables_unverified.
  • Cabeçalhos e rodapés corridos permanecem no texto, a menos que repeatedLines: 'remove', que lista as linhas removidas por página.
  • maxPages (padrão 1000) e timeBudgetMs (padrão 60 000, verificado antes de cada página) param com um aviso page_cap ou time_budget e as páginas lidas até então. Entrada criptografada, malformada e não-PDF retorna { ok: false, error: { code, message } } em vez de lançar erro.

É verificado em 10 relatórios públicos, seis deles os PDFs do usuário semente: manifesto, node research/pdf-corpus/run.mjs, execuções em research/pdf-corpus/runs/.

Scrape, lote e crawl usam o parsers do Firecrawl para escolher como um PDF é lido (REST, SDK, MCP e /fc); outros arquivos não são afetados:

  • Ausente: a camada de texto de cada PDF, com os padrões acima e marcadores de página.
  • []: sem texto PDF. O arquivo é salvo como recebido e o resultado é success com markdown: null e um aviso de arquivo pdf_not_parsed, como para uma planilha.
  • Uma entrada pdf, a string "pdf" ou { "type": "pdf", "mode", "maxPages", "pages", "pageMarkers" }:
    • mode é fast ou auto, ambos o leitor de camada de texto; ocr e o analisador image são recusados com HTTP 400 pelo nome, já que o Octocrawl não executa OCR.
    • maxPages (1 a 10 000) lê as primeiras páginas. Um documento cortado pelo próprio maxPages da requisição permanece success, com file.pdf.pagesRead, pageCount e um aviso page_cap dizendo quanto foi lido; apenas o corte do limite padrão é partial.
    • pages: true adiciona pages: [{ pageNumber, markdown }], o texto de cada página como o Markdown o tem, sem seu marcador (respostas de scrape completas e compactas, itens de lote, páginas de crawl, /fc data.pages).
    • pageMarkers: false deixa as linhas <!-- page N --> de fora; os offsets file.pdf.pages ainda localizam cada página no Markdown. Nativamente, eles estão ligados por padrão; em /fc eles estão desligados a menos que solicitados, como no Firecrawl.

Uma segunda entrada, uma chave desconhecida ou um valor fora do intervalo é HTTP 400 nomeando-o.

Benchmark

Execute a suíte de fixtures completa contra a linha de base HTTP pura:

npm run bench

Saída esperada:

Subject: bare-http
  Cases: 30
  Status matches: 17/30
  Contentful: 20
  False successes: 12
  False success rate: 60.0%

A linha de base HTTP pura tem intencionalmente uma alta taxa de falso sucesso (sem extração de conteúdo, sem detecção de desafio, sem tratamento de redirecionamento). Um sujeito de produção deve superar esses números.

Throughput e recuperação de falha

npm run bench:throughput mede páginas por minuto e tempo por página através do processo da API em um site de loopback com 20 hosts: a via HTTP com 32 workers sobre 1.000 URLs, e a via de navegador com 8 sobre 200. Os primeiros resultados, com o que deixam de fora, estão em docs/benchmarks/2026-10-03-throughput.md. npm run verify:batch-crash-1000 encerra a API com kill -9 no meio de um lote de 1.000 URLs em 20 hosts, inicia-a novamente na mesma raiz de tarefa e verifica se o lote retomado não perde nenhuma URL e não registra nenhuma duas vezes. npm run verify:serve-smoke inicia octocrawl serve, coleta uma página e um PDF, interrompe o servidor no meio de um lote, inicia-o novamente e verifica se o lote é concluído. O CI executa o teste de falha no Linux e o teste de serviço no Windows. Todos os três precisam dos pacotes compilados (npx tsc -b).

O mecanismo da API executa W2L_WORKER_COUNT páginas por vez, um inteiro de 1 a 64 (padrão 4). Esse limite fica acima dos limites por host: W2L_PER_HOST_CONCURRENCY e W2L_PER_HOST_MIN_DELAY_MS ainda se aplicam a cada host, então mais workers ajudam apenas entre hosts. O maxConcurrency de um rastreamento pode chegar até a contagem de workers.

Estrutura do Repositório

packages/
  contracts/       TypeScript types and ground-truth schema (MIT)
  fixtures/        HTTP server with 56 ground-truth test cases
  http-core/       robots.txt parser (ReDoS-resistant)
  runtime/         TaskStore, frontier, bounded crawl orchestrator
  bench/           Benchmark runner, ladder CLI (w2l-ladder), scoring
  cli/             w2l: scrape, crawl, batch, map, serve (AGPL)
  api/             REST server (AGPL)
  sdk/             TypeScript client (MIT)
  mcp/             stdio and restricted Streamable HTTP MCP server (AGPL)

python/            Python client octocrawl-client (MIT)

examples/monitor-workflow.ts       Runnable Monitor + Delivery SDK workflow
examples/webhook-receiver.ts       Durable idempotent sample receiver

ROADMAP.md                         Current phase plan

docs/
  onboarding.md                  Install, Crawl, Monitor, HTTPS events and recovery
  mcp-first-use.md               Conversational Monitor/Delivery and hosted pilot setup
  independent-developer-acceptance.md  Pending human Gate 4 run sheet
  roadmap/section-a-foundation.md  Section A phases and A4 gate
  roadmap/section-b-continuous-data.md  Section B future direction
  roadmap/section-c-delivery.md    Section C future delivery direction
  archive/                        Earlier plans and the hosted-pilot setup: PHASE1_ENGINEERING_NOTES.md (decision log), PRODUCT_PLAN_V2.md, PRODUCT_STRATEGY.md, hosted-mcp-pilot.md, render.yaml
  firecrawl-shim.md               Firecrawl v1 scrape/crawl snapshot + diffs
  benchmark-gate.md               Phase 3 comparator versions, evidence contract, and blockers

Roadmap

  • Contratos e esquema de ground-truth
  • Servidor de fixtures com 56 casos de ground-truth
  • Correção do ReDoS do robots.txt (matcher de glob baseado em tokens)
  • Pipeline de benchmark com baseline HTTP puro
  • extract-tf + HTML→Markdown após extração
  • Via de navegador (Playwright) e escada HTTP → navegador → fornecedor
  • Pacote de identidade honesta (UA / dicas / locale / viewport devem concordar)
  • CLI de produto octocrawl (@octocrawl/cli, octocrawl): scrape, crawl, batch, map e serve, toda opção da API como flag
  • octocrawl crawl + retomada de checkpoint com SQLite
  • API REST + SDK TypeScript (POST /v1/scrape, POST /v1/crawl, GET /v1/crawl/:id, páginas/erros de rastreamento paginados, cancelamento)
  • Servidor MCP (scrape, crawl, get_crawl, páginas/erros paginados e cancelamento via REST)
  • Respostas MCP de coleta compactas, extração direta de JSON/JSON Schema estruturado e adaptador/baseline de assunto da Amazon
  • Shim de migração /scrape /crawl do Firecrawl (snapshot 2026-09-18; não é uma camada de compatibilidade)
  • Contabilidade de escada em nível de tarefa, tentativas preservadas por canal e campos honestos de custo/evidência desconhecidos
  • Workers de múltiplas páginas limitados, agendamento compartilhado de hosts, estabilização condicional do navegador e reutilização de recursos em tempo de execução
  • Fase 1 Portão de Confiabilidade Local: suíte de testes completa com Chromium e GitHub Actions
  • Fase 2 Benchmark de qualidade L0-L2: escada Octocrawl, conclusão verificada, falso-sucesso, P95, escalonamento e relatórios em camadas
  • Fase A4 harness de tarefas reais: manifestos de conhecimento de IA e informações de produto, asserções de campo, consistência de repetição, registros de holdout e custo/evidência
  • Fase A4 portão de tarefas reais: 100-200 páginas permitidas, tempo de correção humana, evidência de tarefas repetidas e taxonomia completa de falhas
  • Fase A4 expansão diagnóstica: 20 tarefas reais, 11 domínios, 40 execuções repetidas e resultados de holdout
  • Fatia de escala A6: 100 páginas, 10 domínios, duas execuções; holdout rotulado não é independente
  • Evidências de recuperação/instalação A6 registradas: interromper-retomar perdeu 0 URLs; primeira tarefa de clone limpo na mesma máquina; registro histórico de correção de 18 minutos carece de confirmação humana; USD cobrado desconhecido
  • Exceções adiadas A6 registradas: instalação por segundo desenvolvedor é adiada, não aprovada; USD cobrado é desconhecido, não zero
  • Aprovação incondicional A6 ainda precisa de uma segunda instalação humana
  • Relatório de portão A5/A6: alfa condicional; USD cobrado permanece desconhecido
  • Contrato de execução do Portão 2, recuperação real de processo, mudanças controladas/cache e isolamento do Monitor
  • Entrega HTTPS durável do Portão 3, nova tentativa no mesmo evento, deduplicação e recuperação de reinício
  • Portão 4 SDK, docs, exemplos e instalação limpa de agente
  • Portão 4 instalação humana independente de não-autor e fluxo de trabalho completo
  • C2 Monitor/Delivery MCP e fluxo conversacional local de primeiro uso
  • C2 n8n e UI de tarefa estreita
  • C3 processo unificado de instância única e implementação autenticada de Streamable HTTP
  • C3 MCP hospedado: implantação, aceitação de login e exercício de reinício hospedado (código experimental; configuração arquivada em docs/archive/hosted-mcp-pilot.md; hospedagem é um item P5)
  • Portão 5 dois usuários de teste externos, duas semanas, uso repetido e consumo real a jusante
  • Fase 3 harness do Portão de Benchmark: execução fixa do Octocrawl, evidência de comparador e decisão bloqueada até comparadores reais
  • Portão de Egresso Hospedado: aplicação de política de subrecursos do navegador e vinculação de DNS à conexão

Veja ROADMAP.md para o plano de fase atual; o roadmap das Seções A/B/C está arquivado em docs/roadmap/sections-abc-roadmap-2026-09-28.md. PRODUCT_PLAN_V2.md permanece como o plano detalhado histórico.

Contribuindo

Usamos o Developer Certificate of Origin (DCO) em vez de um CLA. Todo commit precisa de uma linha Signed-off-by:

git commit -s -m "Your commit message"

Veja CONTRIBUTING.md para detalhes.

Licença

Código do lado do servidor, o CLI (@octocrawl/cli, octocrawl) e o servidor MCP (@octocrawl/mcp): AGPL-3.0
Bibliotecas de cliente: MIT: o SDK TypeScript (@octocrawl/sdk, que inclui o @w2l/contracts do workspace, também MIT) e o cliente Python (octocrawl-client)

Veja PHASE1_ENGINEERING_NOTES.md §1.3 para a justificativa.

Por que AGPL?

A AGPL exige que modificações implantadas em rede permaneçam abertas. Qualquer pessoa pode fazer fork, modificar e hospedar o Octocrawl — desde que compartilhe essas modificações. O verdadeiro diferencial é o nome (marca registrada) e o serviço hospedado, não o bloqueio da licença.