Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2547,7 +2547,7 @@
"proxy": {
"type": "string",
"enum": ["basic", "enhanced", "auto"],
"description": "Specifies the type of proxy to use.\n\n - **basic**: Proxies for scraping sites with none to basic anti-bot solutions. Fast and usually works.\n - **enhanced**: Enhanced proxies for scraping sites with advanced anti-bot solutions. Slower, but more reliable on certain sites. Costs up to 5 credits per request.\n - **auto**: Firecrawl will automatically retry scraping with enhanced proxies if the basic proxy fails. If the retry with enhanced is successful, 5 credits will be billed for the scrape. If the first attempt with basic is successful, only the regular cost will be billed.\n\nIf you do not specify a proxy, Firecrawl will default to basic."
"description": "Specifies the type of proxy to use.\n\n - **basic**: Proxies for scraping sites with none to basic anti-bot solutions. Fast and usually works.\n - **enhanced**: Enhanced proxies for scraping sites with advanced anti-bot solutions. Slower, but more reliable on certain sites. Billed at the same credit cost as basic.\n - **auto**: Firecrawl will automatically retry scraping with enhanced proxies if the basic proxy fails. Enhanced proxies carry no credit surcharge, so either way only the regular cost is billed.\n\nIf you do not specify a proxy, Firecrawl will default to basic."
},
"changeTrackingOptions": {
"type": "object",
Expand Down
57 changes: 55 additions & 2 deletions apps/api/src/__tests__/snips/v2/billing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,55 @@ describeIf(TEST_PRODUCTION)("Billing tests", () => {
120000,
);

it.concurrent(
"bills enhanced proxy scrapes at the base rate",
async () => {
const identity = await idmux({
name: "billing/bills enhanced proxy scrapes at the base rate",
credits: 100,
});

const rc1 = (await creditUsage(identity)).remainingCredits;

const [basicScrape, enhancedScrape] = await Promise.all([
scrape(
{
url: TEST_SUITE_WEBSITE,
proxy: "basic",
},
identity,
),

scrape(
{
url: TEST_SUITE_WEBSITE,
proxy: "enhanced",
},
identity,
),
]);

// Guard against a vacuous pass: the enhanced request must actually have
// run on the enhanced proxy for the credit assertion to mean anything.
// `proxyUsed` still reports that proxy under its original wire value.
expect(basicScrape.metadata.proxyUsed).toBe("basic");
expect(enhancedScrape.metadata.proxyUsed).toBe("stealth");

// Enhanced proxies carry no surcharge: same 1 credit as basic.
expect(basicScrape.metadata.creditsUsed).toBe(1);
expect(enhancedScrape.metadata.creditsUsed).toBe(1);

// sum: 2 credits

await sleepForBatchBilling();

const rc2 = (await creditUsage(identity)).remainingCredits;

expect(rc1 - rc2).toBe(2);
},
180000,
);

it.concurrent(
"bills parse correctly",
async () => {
Expand Down Expand Up @@ -627,8 +676,12 @@ describeIf(TEST_PRODUCTION)("Billing tests", () => {

// Verify periods are sorted by startDate ascending
for (let i = 1; i < result.periods.length; i++) {
const prevRaw = result.periods[i - 1].startDate ? Date.parse(result.periods[i - 1].startDate!) : NaN;
const currRaw = result.periods[i].startDate ? Date.parse(result.periods[i].startDate!) : NaN;
const prevRaw = result.periods[i - 1].startDate
? Date.parse(result.periods[i - 1].startDate!)
: NaN;
const currRaw = result.periods[i].startDate
? Date.parse(result.periods[i].startDate!)
: NaN;
const prevNaN = Number.isNaN(prevRaw);
const currNaN = Number.isNaN(currRaw);
if (!prevNaN && !currNaN) {
Expand Down
8 changes: 0 additions & 8 deletions apps/api/src/lib/keyless-credit-projection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,6 @@ export function projectScrapeCredits(
credits += 4;
}

if (
options.proxy === "stealth" ||
options.proxy === "enhanced" ||
options.proxy === "auto"
) {
credits += 4;
}

return credits;
}

Expand Down
55 changes: 55 additions & 0 deletions apps/api/src/lib/scrape-billing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,61 @@ describe("calculateCreditsToBeBilled", () => {
expect(credits).toBe(30);
});

it("bills enhanced proxy scrapes the same as basic ones", async () => {
const bill = (unsupportedFeatures?: Set<any>) =>
calculateCreditsToBeBilled(
{
formats: [{ type: "markdown" }],
} as any,
{
teamId: "team-id",
orgId: null,
},
{
metadata: {
statusCode: 200,
proxyUsed: "stealth",
},
} as any,
{
totalCost: 0,
} as any,
{} as any,
undefined,
unsupportedFeatures,
);

// No surcharge, whether or not the engine could honour Enhanced Mode (the
// old waiver for an unsupported enhanced proxy is moot now there is
// nothing to waive).
expect(await bill()).toBe(1);
expect(await bill(new Set(["stealthProxy"]))).toBe(1);
});

it("still bills enhanced proxy scrapes with json at 5 credits", async () => {
const credits = await calculateCreditsToBeBilled(
{
formats: [{ type: "json", schema: {} }],
} as any,
{
teamId: "team-id",
orgId: null,
},
{
metadata: {
statusCode: 200,
proxyUsed: "stealth",
},
} as any,
{
totalCost: 0,
} as any,
{} as any,
);

expect(credits).toBe(5);
});

it("bills deterministic JSON at 10 credits when the script was generated", async () => {
const credits = await calculateCreditsToBeBilled(
{
Expand Down
16 changes: 6 additions & 10 deletions apps/api/src/lib/scrape-billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import type { ThreatDecision } from "./threat-protection/types";
import { UnsafeDomainBlockedError } from "./threat-protection/error";

const creditsPerPDFPage = 1;
const stealthProxyCostBonus = 4;
const unblockedDomainCostBonus = 4;
const xTwitterCostBonus = 29;
const redactPIICostBonus = 4;
Expand Down Expand Up @@ -68,7 +67,11 @@ export async function calculateCreditsToBeBilled(
costTracking: CostTracking | ReturnType<typeof CostTracking.prototype.toJSON>,
flags: TeamFlags,
error?: Error | null,
unsupportedFeatures?: Set<FeatureFlag>,
// Unused by billing today (Enhanced Mode proxies no longer carry a
// surcharge, so there is nothing to waive when the engine could not honour
// the feature). Kept because callers pass `exchange` and `threatDecisions`
// positionally after it.
_unsupportedFeatures?: Set<FeatureFlag>,
exchange?: ExchangeScrapeMetadata,
// Threat protection decisions for this scrape (initial + redirect checks,
// in order). Each decision with `providerConsulted` bills a scan fee (+2
Expand Down Expand Up @@ -198,7 +201,7 @@ export async function calculateCreditsToBeBilled(
}

if (options.redactPII) {
// Flat +4 to match lockdown / audio / video / stealth — fire-privacy
// Flat +4 to match lockdown / audio / video — fire-privacy
// is a peer premium feature, not a cost-based one. PDF pages all
// pass through redaction too, so each additional page picks up
// another +4 on top of the +1 page parse cost.
Expand All @@ -208,13 +211,6 @@ export async function calculateCreditsToBeBilled(
}
}

if (
document?.metadata?.proxyUsed === "stealth" &&
!unsupportedFeatures?.has("stealthProxy") // if stealth proxy was unsupported, don't bill for it
) {
creditsToBeBilled += stealthProxyCostBonus;
}

const urlsToCheck = [
document.metadata?.url,
document.metadata?.sourceURL,
Expand Down
17 changes: 9 additions & 8 deletions apps/api/src/services/monitoring/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ describe("monitoring store credit helpers", () => {
},
];

expect(estimateMonitorCreditsPerRun(targets, false)).toBe(18);
expect(estimateMonitorCreditsPerRun(targets, true)).toBe(20);
// 2 URLs x 5 (json change tracking). Enhanced proxies add nothing.
expect(estimateMonitorCreditsPerRun(targets, false)).toBe(10);
expect(estimateMonitorCreditsPerRun(targets, true)).toBe(12);
});

it("adds predictable lockdown costs and judge credits separately", () => {
Expand Down Expand Up @@ -69,7 +70,7 @@ describe("monitoring store credit helpers", () => {
],
targets,
),
).toBe(10);
).toBe(6);
});

it("uses monitor metadata for fallback PDF credits when recorded usage is missing", () => {
Expand All @@ -96,7 +97,7 @@ describe("monitoring store credit helpers", () => {
).toBe(5);
});

it("uses monitor metadata for fallback proxy and postprocessor credits", () => {
it("uses monitor metadata for fallback postprocessor credits and never for proxies", () => {
const targets: MonitorTarget[] = [
{
id: "target-1",
Expand All @@ -120,10 +121,10 @@ describe("monitoring store credit helpers", () => {
],
targets,
),
).toBe(34);
).toBe(30);
});

it("treats enhanced proxy metadata as premium for fallback billing", () => {
it("does not bill extra when enhanced proxy metadata is present", () => {
const targets: MonitorTarget[] = [
{
id: "target-1",
Expand All @@ -146,10 +147,10 @@ describe("monitoring store credit helpers", () => {
],
targets,
),
).toBe(5);
).toBe(1);
});

it("does not add fallback proxy credits when runtime metadata says basic was used", () => {
it("bills json and extra PDF pages in fallback billing, whatever proxy ran", () => {
const targets: MonitorTarget[] = [
{
id: "target-1",
Expand Down
26 changes: 1 addition & 25 deletions apps/api/src/services/monitoring/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ const MONITOR_CHECK_PAGE_BATCH_SIZE = 1000;
type MonitorCreditMetadata = {
creditsUsed?: unknown;
numPages?: unknown;
proxyUsed?: unknown;
postprocessorsUsed?: unknown;
};

Expand Down Expand Up @@ -95,10 +94,8 @@ function requestsJsonChangeTracking(formats: unknown): boolean {

function estimateBaseCreditsPerPage(
options: MonitorTarget["scrapeOptions"],
params: { includeProxy?: boolean } = {},
): number {
const formats = options?.formats;
const includeProxy = params.includeProxy ?? true;
const usesDeterministicJson = hasFormatOfType(formats, "deterministicJson");
const usesJsonCredits =
hasFormatOfType(formats, "json") || requestsJsonChangeTracking(formats);
Expand Down Expand Up @@ -131,13 +128,6 @@ function estimateBaseCreditsPerPage(
credits += SCRAPE_OPTION_CREDIT_BONUS;
}

if (
includeProxy &&
(options?.proxy === "stealth" || options?.proxy === "enhanced")
) {
credits += SCRAPE_OPTION_CREDIT_BONUS;
}

return credits;
}

Expand Down Expand Up @@ -230,9 +220,7 @@ export function calculateMonitorCheckActualCreditsFromPages(
const baseCreditsByTarget = new Map(
targets.map(target => [
target.id,
estimateBaseCreditsPerPage(target.scrapeOptions, {
includeProxy: false,
}),
estimateBaseCreditsPerPage(target.scrapeOptions),
]),
);
const targetsById = new Map(targets.map(target => [target.id, target]));
Expand Down Expand Up @@ -263,18 +251,6 @@ export function calculateMonitorCheckActualCreditsFromPages(
credits += metadata.numPages - 1;
}

const requestedPremiumProxy =
target?.scrapeOptions?.proxy === "stealth" ||
target?.scrapeOptions?.proxy === "enhanced";
const usedPremiumProxy =
metadata?.proxyUsed === "stealth" || metadata?.proxyUsed === "enhanced";
if (
usedPremiumProxy ||
(metadata?.proxyUsed == null && requestedPremiumProxy)
) {
credits += SCRAPE_OPTION_CREDIT_BONUS;
}

if (
Array.isArray(metadata?.postprocessorsUsed) &&
metadata.postprocessorsUsed.includes("x-twitter")
Expand Down
2 changes: 1 addition & 1 deletion apps/api/v1-openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2367,7 +2367,7 @@
"proxy": {
"type": "string",
"enum": ["basic", "enhanced", "auto"],
"description": "Specifies the type of proxy to use.\n\n - **basic**: Proxies for scraping sites with none to basic anti-bot solutions. Fast and usually works.\n - **enhanced**: Enhanced proxies for scraping sites with advanced anti-bot solutions. Slower, but more reliable on certain sites. Costs up to 5 credits per request.\n - **auto**: Firecrawl will automatically retry scraping with enhanced proxies if the basic proxy fails. If the retry with enhanced is successful, 5 credits will be billed for the scrape. If the first attempt with basic is successful, only the regular cost will be billed.\n\nIf you do not specify a proxy, Firecrawl will default to basic."
"description": "Specifies the type of proxy to use.\n\n - **basic**: Proxies for scraping sites with none to basic anti-bot solutions. Fast and usually works.\n - **enhanced**: Enhanced proxies for scraping sites with advanced anti-bot solutions. Slower, but more reliable on certain sites. Billed at the same credit cost as basic.\n - **auto**: Firecrawl will automatically retry scraping with enhanced proxies if the basic proxy fails. Enhanced proxies carry no credit surcharge, so either way only the regular cost is billed.\n\nIf you do not specify a proxy, Firecrawl will default to basic."
},
"changeTrackingOptions": {
"type": "object",
Expand Down
4 changes: 2 additions & 2 deletions apps/elixir-sdk/lib/firecrawl.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1033,7 +1033,7 @@ defmodule Firecrawl do
only_main_content: [type: :boolean, doc: "Only return the main content of the page excluding headers, navs, footers, etc."],
parsers: [type: {:list, :any}, doc: "Controls how files are processed during scraping. When \"pdf\" is included (default), the PDF content is extracted and converted to markdown format, with billing based on the number of pages (1 credit per page). When an empty array is passed, the PDF file is returned in base64 encoding with a flat rate of 1 credit for the entire PDF."],
profile: [type: :keyword_list, doc: "Enable persistent browser storage across scrape and interact sessions. Pass a profile when scraping to preserve cookies, localStorage, and session data. Sessions with the same profile name share browser state."],
proxy: [type: {:in, [:basic, :enhanced, :auto]}, doc: "Specifies the type of proxy to use.\n\n - **basic**: Proxies for scraping sites with none to basic anti-bot solutions. Fast and usually works.\n - **enhanced**: Enhanced proxies for scraping sites with advanced anti-bot solutions. Slower, but more reliable on certain sites. Costs up to 5 credits per request.\n - **auto**: Firecrawl will automatically retry scraping with enhanced proxies if the basic proxy fails. If the retry with enhanced is successful, 5 credits will be billed for the scrape. If the first attempt with basic is successful, only the regular cost will be billed."],
proxy: [type: {:in, [:basic, :enhanced, :auto]}, doc: "Specifies the type of proxy to use.\n\n - **basic**: Proxies for scraping sites with none to basic anti-bot solutions. Fast and usually works.\n - **enhanced**: Enhanced proxies for scraping sites with advanced anti-bot solutions. Slower, but more reliable on certain sites. Billed at the same credit cost as basic.\n - **auto**: Firecrawl will automatically retry scraping with enhanced proxies if the basic proxy fails. Enhanced proxies carry no credit surcharge, so either way only the regular cost is billed."],
redact_pii: [type: :boolean, doc: "Redact personally identifiable information from returned content."],
remove_base64_images: [type: :boolean, doc: "Removes all base 64 images from the markdown output, which may be overwhelmingly long. This does not affect html or rawHtml formats. The image's alt text remains in the output, but the URL is replaced with a placeholder."],
skip_tls_verification: [type: :boolean, doc: "Skip TLS certificate verification when making requests."],
Expand Down Expand Up @@ -1100,7 +1100,7 @@ defmodule Firecrawl do
only_main_content: [type: :boolean, doc: "Only return the main content of the page excluding headers, navs, footers, etc."],
parsers: [type: {:list, :any}, doc: "Controls how files are processed during scraping. When \"pdf\" is included (default), the PDF content is extracted and converted to markdown format, with billing based on the number of pages (1 credit per page). When an empty array is passed, the PDF file is returned in base64 encoding with a flat rate of 1 credit for the entire PDF."],
profile: [type: :keyword_list, doc: "Enable persistent browser storage across scrape and interact sessions. Pass a profile when scraping to preserve cookies, localStorage, and session data. Sessions with the same profile name share browser state."],
proxy: [type: {:in, [:basic, :enhanced, :auto]}, doc: "Specifies the type of proxy to use.\n\n - **basic**: Proxies for scraping sites with none to basic anti-bot solutions. Fast and usually works.\n - **enhanced**: Enhanced proxies for scraping sites with advanced anti-bot solutions. Slower, but more reliable on certain sites. Costs up to 5 credits per request.\n - **auto**: Firecrawl will automatically retry scraping with enhanced proxies if the basic proxy fails. If the retry with enhanced is successful, 5 credits will be billed for the scrape. If the first attempt with basic is successful, only the regular cost will be billed."],
proxy: [type: {:in, [:basic, :enhanced, :auto]}, doc: "Specifies the type of proxy to use.\n\n - **basic**: Proxies for scraping sites with none to basic anti-bot solutions. Fast and usually works.\n - **enhanced**: Enhanced proxies for scraping sites with advanced anti-bot solutions. Slower, but more reliable on certain sites. Billed at the same credit cost as basic.\n - **auto**: Firecrawl will automatically retry scraping with enhanced proxies if the basic proxy fails. Enhanced proxies carry no credit surcharge, so either way only the regular cost is billed."],
redact_pii: [type: :boolean, doc: "Redact personally identifiable information from returned content."],
remove_base64_images: [type: :boolean, doc: "Removes all base 64 images from the markdown output, which may be overwhelmingly long. This does not affect html or rawHtml formats. The image's alt text remains in the output, but the URL is replaced with a placeholder."],
skip_tls_verification: [type: :boolean, doc: "Skip TLS certificate verification when making requests."],
Expand Down
4 changes: 2 additions & 2 deletions apps/python-sdk/firecrawl/v1/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,7 @@ def scrape_url(
skip_tls_verification (Optional[bool]): Skip TLS verification
remove_base64_images (Optional[bool]): Remove base64 images
block_ads (Optional[bool]): Block ads
proxy (Optional[Literal["basic", "stealth", "auto"]]): Proxy type (basic/stealth)
proxy (Optional[Literal["basic", "stealth", "enhanced", "auto"]]): Proxy type (basic/enhanced)
extract (Optional[JsonConfig]): Content extraction settings
json_options (Optional[JsonConfig]): JSON extraction settings
actions (Optional[List[Union[WaitAction, ScreenshotAction, ClickAction, WriteAction, PressAction, ScrollAction, ScrapeAction, ExecuteJavascriptAction, PDFAction]]]): Actions to perform
Expand Down Expand Up @@ -3649,7 +3649,7 @@ async def scrape_url(
skip_tls_verification (Optional[bool]): Skip TLS verification
remove_base64_images (Optional[bool]): Remove base64 images
block_ads (Optional[bool]): Block ads
proxy (Optional[Literal["basic", "stealth", "auto"]]): Proxy type (basic/stealth)
proxy (Optional[Literal["basic", "stealth", "enhanced", "auto"]]): Proxy type (basic/enhanced)
extract (Optional[V1JsonConfig]): Content extraction settings
json_options (Optional[V1JsonConfig]): JSON extraction settings
actions (Optional[List[Union[V1WaitAction, V1ScreenshotAction, V1ClickAction, V1WriteAction, V1PressAction, V1ScrollAction, V1ScrapeAction, V1ExecuteJavascriptAction, V1PDFAction]]]): Actions to perform
Expand Down
Loading