@@ -304,11 +304,19 @@ const server = new FastMCP<SessionData>({
304304
305305 if (process.env.CLOUD_SERVICE === 'true') {
306306 if (!headerCred) {
307- // Keyless free tier over the hosted MCP: allowed only when a forwarding
308- // secret is configured AND we can determine the end-user's client IP, so
309- // the API rate-limits per real client IP rather than the shared server IP.
307+ // Keyless free tier over the hosted MCP: serve it only when a forwarding
308+ // secret is configured, we know the end-user's client IP (so the API can
309+ // rate-limit per real IP, not the shared server IP), AND that IP still
310+ // has free quota. If the IP is out of quota (or keyless is off), fall
311+ // through to throw so FastMCP emits the OAuth 401 + WWW-Authenticate
312+ // challenge — i.e. prompt the user to connect an account exactly when
313+ // their free quota runs out.
310314 const clientIp = extractClientIp(request);
311- if (process.env.KEYLESS_PROXY_SECRET && clientIp) {
315+ if (
316+ process.env.KEYLESS_PROXY_SECRET &&
317+ clientIp &&
318+ (await keylessEligible(clientIp))
319+ ) {
312320 return { firecrawlApiKey: undefined, research, keylessClientIp: clientIp };
313321 }
314322 throw new Error(
@@ -961,6 +969,33 @@ function extractClientIp(request?: {
961969 return first || undefined ;
962970}
963971
972+ /**
973+ * Read-only check (no quota consumed) of whether a client IP can still use the
974+ * keyless free tier, via the API's secret-gated eligibility endpoint. Fails
975+ * closed: anything other than a clear "eligible: true" means fall through to the
976+ * OAuth challenge rather than silently granting keyless.
977+ */
978+ async function keylessEligible ( clientIp : string ) : Promise < boolean > {
979+ const secret = process . env . KEYLESS_PROXY_SECRET ;
980+ if ( ! secret ) return false ;
981+ try {
982+ const response = await fetch (
983+ `${ resolveApiBaseUrl ( ) } /v2/keyless/eligibility` ,
984+ {
985+ headers : {
986+ 'x-firecrawl-keyless-ip' : clientIp ,
987+ 'x-firecrawl-keyless-secret' : secret ,
988+ } ,
989+ }
990+ ) ;
991+ if ( ! response . ok ) return false ;
992+ const json : any = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
993+ return json ?. eligible === true ;
994+ } catch {
995+ return false ;
996+ }
997+ }
998+
964999function isKeylessMode ( session ? : SessionData ) : boolean {
9651000 if ( session ?. firecrawlApiKey ) return false ;
9661001 if ( process . env . CLOUD_SERVICE === 'true' ) {
0 commit comments