npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

crawlforge-sdk

v0.1.1

Published

Official TypeScript SDK for the CrawlForge REST API: 30 metered web tools behind one API key.

Readme

crawlforge-sdk

The official TypeScript SDK for the CrawlForge REST API: 30 metered web tools (fetch, scrape, search, crawl, extract, research) behind one API key.

  • One typed method per tool, generated from the OpenAPI specification.
  • Typed errors for validation, authentication, credits, rate limits and tool failures.
  • Retries on 429 only, honouring Retry-After.
  • creditsUsed and creditsRemaining on every result.
  • ESM and CJS, no runtime dependencies, fetch only: Node 18+ and edge runtimes.

API keys: https://www.crawlforge.dev/dashboard/keys

Install

npm install crawlforge-sdk

Quick start

import { CrawlForge } from 'crawlforge-sdk';

const client = new CrawlForge({ apiKey: process.env.CRAWLFORGE_API_KEY });

const page = await client.scrape({
  url: 'https://example.com',
  formats: ['markdown', 'links'],
});

console.log(page.data.formats);        // tool-specific payload
console.log(page.creditsUsed);         // 2
console.log(page.creditsRemaining);    // your balance after this call

new CrawlForge() with no options reads CRAWLFORGE_API_KEY from the environment when a process.env exists, and throws a CrawlForgeError (code MISSING_API_KEY) at construction when no key is found.

Options

new CrawlForge({
  apiKey: '...',                                   // or CRAWLFORGE_API_KEY
  baseUrl: 'https://www.crawlforge.dev/api/v1',   // default
  maxRetries: 2,                                   // retries after a 429; 2 = up to three requests
  timeoutMs: 60_000,                               // per attempt
  fetch: customFetch,                              // replaces globalThis.fetch
});

Every method also takes per-call options: { signal?: AbortSignal; timeoutMs?: number; maxRetries?: number }.

const controller = new AbortController();
const result = await client.searchWeb({ query: 'web scraping' }, { signal: controller.signal, timeoutMs: 20_000 });

Tools

One method per tool. credits is each tool's base price from the spec; a tool with a per-unit or conditional price says so in the credits column and on its docs page. Request parameters are typed (ScrapeRequest, SearchWebRequest, ...); the data of a result is tool-specific and typed as Record<string, unknown> in this release, so read the response shape on the tool's docs page.

| Method | Tool | Credits | Docs | |---|---|---|---| | agent() | agent | 8 | agent | | analyzeContent() | analyze_content | 3 | analyze_content | | batchScrape() | batch_scrape | 5; 5 per URL attempted (skipped URLs are not charged) | batch_scrape | | crawlDeep() | crawl_deep | 4 | crawl_deep | | deepResearch() | deep_research | 10 | deep_research | | extractContent() | extract_content | 2 | extract_content | | extractEmbeddedState() | extract_embedded_state | 2 | extract_embedded_state | | extractLinks() | extract_links | 1 | extract_links | | extractMetadata() | extract_metadata | 1 | extract_metadata | | extractStructured() | extract_structured | 3 | extract_structured | | extractText() | extract_text | 1 | extract_text | | extractWithLlm() | extract_with_llm | 3 | extract_with_llm | | fetchUrl() | fetch_url | 1 | fetch_url | | generateLlmsTxt() | generate_llms_txt | 5 | generate_llms_txt | | getBatchResults() | get_batch_results | 1 | get_batch_results | | listOllamaModels() | list_ollama_models | 1 | list_ollama_models | | localization() | localization | 2 | localization | | mapSite() | map_site | 2 | map_site | | processDocument() | process_document | 2 | process_document | | readResult() | read_result | 1 | read_result | | redditSearch() | reddit_search | 5 | reddit_search | | scrape() | scrape | 2 | scrape | | scrapeStructured() | scrape_structured | 2 | scrape_structured | | scrapeTemplate() | scrape_template | 1 | scrape_template | | scrapeWithActions() | scrape_with_actions | 5 | scrape_with_actions | | searchWeb() | search_web | 5 | search_web | | serpRank() | serp_rank | 5 | serp_rank | | stealthMode() | stealth_mode | 5 | stealth_mode | | summarizeContent() | summarize_content | 4 | summarize_content | | trackChanges() | track_changes | 3 | track_changes |

Any tool can also be called by name, and its self-description fetched without a key:

const result = await client.call('fetch_url', { url: 'https://example.com' });
const info = await client.describe('fetch_url'); // price, JSON Schema, example body

The TOOLS export lists every tool with its method name, credits and docs URL.

Results

Every call resolves to a ToolResult:

interface ToolResult<T = Record<string, unknown>> {
  data: T;                  // the tool's payload
  creditsUsed: number;      // charged for this call
  creditsRemaining: number; // balance after this call
  processingTime: number;   // server-side, in ms
  warnings: string[];       // non-fatal notes; [] when there are none
}

client.call<T>() accepts a type argument for data when you know a tool's shape:

const page = await client.call<{ status: number; content: string }>('fetch_url', { url: 'https://example.com' });
page.data.status; // number

Errors

Every failure throws a subclass of CrawlForgeError with status, code, message, details and, when the response carried one, requestId. Credits are deducted only after a call succeeds, so an error response never charges.

| Status | Class | Extra fields | |---|---|---| | 400 | ValidationError | details is the validation issue list | | 401 | AuthenticationError | | | 402 | InsufficientCreditsError | autoRechargeTriggered (from X-Auto-Recharge-Triggered) | | 429 | RateLimitError | retryAfter in seconds, or null without a Retry-After header | | other | ToolError | blocked ({ vendor, evidence } or null), escalated |

A response that is not JSON (an edge error page) becomes a CrawlForgeError with code UNEXPECTED_RESPONSE and the raw body in details; a transport failure has code NETWORK_ERROR and status 0; a per-attempt timeout has code TIMEOUT. An AbortSignal you pass rejects with its own reason.

import { CrawlForge, InsufficientCreditsError, RateLimitError, ValidationError } from 'crawlforge-sdk';

try {
  await client.fetchUrl({ url: 'not a url' });
} catch (err) {
  if (err instanceof ValidationError) console.error(err.details);           // what was wrong
  else if (err instanceof InsufficientCreditsError) console.error(err.autoRechargeTriggered);
  else if (err instanceof RateLimitError) console.error(err.retryAfter);   // seconds, or null
  else throw err;
}

Errors never contain the API key.

Retries

Only a 429 is retried. The client waits Retry-After seconds when the header is present (integer or HTTP date, capped at 60 s), otherwise 1 s, 2 s, 4 s ... with ±20 % jitter. maxRetries counts retries, not attempts: the default of 2 allows three requests, after which the RateLimitError is thrown. Both RATE_LIMIT_EXCEEDED (your plan's limit) and HOST_BACKOFF (the target site asked CrawlForge to wait) are 429s; code tells them apart. A 5xx is never retried: a failed call is not charged, but a repeat would run the tool again.

Runtimes

The client uses only fetch, AbortController and timers, with no Node built-ins, so it runs on Node 18+ and on edge runtimes that provide those globals (Cloudflare Workers, Vercel Edge Functions, Deno, Bun). CI exercises Node 18, 20 and 22. Pass { fetch } to use a different implementation.

Keep the API key server-side: a browser would ship it to every visitor.

Spec and generated code

openapi.json in this repository is the published specification; src/generated/ is produced from it by npm run generate (types by openapi-typescript, the tool table and methods by scripts/gen-tools.mjs). Request types are the properties view of each request schema: cross-field rules such as "one of url or html" are enforced by the API and noted in each method's JSDoc.

  • API reference: https://www.crawlforge.dev/docs/api-reference
  • OpenAPI specification: https://www.crawlforge.dev/openapi.json
  • API keys: https://www.crawlforge.dev/dashboard/keys

License

MIT