Unexpected €54k billing spike in 13 hours: Firebase browser key without API restrictions used for Gemini requests

Hello,

We are looking for guidance regarding an unexpected €54,000+ Gemini API charge that occurred within a few hours after enabling Firebase AI Logic on an existing Firebase project.

Background:

We created the project over a year ago and initially used it only for Firebase Authentication. Recently, we added a simple AI feature (generating a web snippet from a text prompt) and enabled Firebase AI Logic.

What happened:

Shortly after enabling this, we experienced a sudden and extreme spike in Gemini API usage. The traffic was not correlated with our actual users and appeared to be automated. The activity occurred within a short overnight window and stopped once we disabled the API and rotated credentials.

Additional observations:

  • We had a budget alert (€80) and a cost anomaly alert, both of which triggered with a delay of a few hours
  • By the time we reacted, costs were already around €28,000
  • The final amount settled at €54,000+ due to delayed cost reporting

This describes our issue in more detail:

Aftermath:

We worked with Google Cloud support and provided logs and analysis. The charges were classified as valid usage because they originated from our project, and our request for a billing adjustment was ultimately denied.

This usage was clearly anomalous, not user-driven, and does not reflect intended or meaningful consumption of the service.

Questions:

  • Has anyone encountered a similar issue after enabling Firebase AI Logic or Gemini?
  • Are there recommended safeguards beyond App Check, quotas, and moving calls server-side?
  • Is there any escalation path we may have missed for cases like this?

Any guidance or shared experience would be greatly appreciated.

Hey @zanbezi ! Sorry to hear about this. A few things:

  1. We have billing account caps rolled out to users of the Gemini API, see: https://ai.google.dev/gemini-api/docs/billing#tier-spend-caps, tier 1 users can spend $250 a month and then are cut off by default (there is a 10 minute delay in all of the reporting)

  2. We now support project spend caps, if you want to set a customer spend cap, you can also do that (I have my account set at $50 so I don’t spend too much accidenlty when building, the same 10 minute delay applies here too): https://ai.google.dev/gemini-api/docs/billing#project-spend-caps

  3. We are moving to disable the usage of unrestricted API keys in the Gemini API, should have more updates there soon.

  4. We now generate Auth keys by default for new users (more secure key which didn’t exist when the Gemini API was originally created a few years ago) and will have more to share there soon.

  5. You should generally avoid putting a key in client side code as if it is exposed, even with the restrictions above you can incur costs.

  6. In many cases, we can automatically detect when a key is visible on the public web and shut down those keys automatically for security reasons (this happened to me personally, I accidentally pushed my API key to the public API docs and it was shut down in minutes).

  7. By default, keys generated in Google AI Studio are restricted to just the Gemini API, no other services are enabled. However keys generated from other parts of Google Cloud have this cross service capability, you can double check keys and make sure they are restricted for just the resource you need.

  8. Pls email me and our team can take a look into this case (Lkilpatrick@google.com), we take this all very serious and have been pushing hard to land all the features mentioned above and more.

  9. We just started the prepaid billing rollout which means you have to pay ahead of time to use the Gemini API, this is rolled out to all new US billing accounts as of yesterday and rolling out globally right now. This is yet another way to give developers more control over their spending / costs and ensure you know what you are signing up for when using the Gemini API.

I hope this helps and sorry for the hassle on this experience, pls email me if there is more to chat about!

Hi,

Thanks for the detailed response, we really appreciate it. It is good to see that additional safeguards (like spend caps) are being introduced.

I will reach out via email with the details so your team can take a closer look.

Thanks again for taking the time to respond.

Great to see you here Logan. This is the proper way to deal with a fiasco like this one.

Good afternoon, I’m glad you’ve started showing signs of activity on the forum again. But you owe us an answer to a pressing question about content blocking - the introduction of a stricter safety filter (which isn’t working correctly, ruining both basic, innocent interactions and creative texts and stories). You asked us to provide examples, and we did, so how long do we have to wait for an official response? Could you please stop running away from us and answer honestly?

I sympathize. We were ucked by the same issue last night

you can use this script to scan your projects

Prerequisites (before running the script):

  • Node.js 18+ (uses native fetch and top-level await)
  • Firebase CLI installed and authenticated: firebase login
  • Google Cloud CLI installed and authenticated: gcloud auth login
  • You must have Owner or Editor access to the GCP projects you want to scan

Usage:

# Scan deployed JS bundles in your local public/ folder only
node check-gemini-key-exposure.mjs

# Scan all GCP projects linked to your gcloud account
node check-gemini-key-exposure.mjs --all-projects
#!/usr/bin/env node
/**
 * check-gemini-key-exposure.mjs
 *
 * Two-mode check:
 *   1. (default) Scans all deployed JS bundles in public/ for Firebase API keys
 *      and tests each against Gemini (TruffleHog PoC).
 *   2. (--all-projects) Uses `gcloud` to enumerate every GCP project, lists all
 *      API keys per project via `gcloud alpha services api-keys list`, and tests
 *      each key against Gemini.
 *
 * Usage:
 *   node scripts/check-gemini-key-exposure.mjs                # public/ scan only
 *   node scripts/check-gemini-key-exposure.mjs --all-projects # all gcloud projects
 *
 * Exit code:
 *   0 - all keys are blocked (safe)
 *   1 - at least one key is exposed (action required)
 */

import { readdirSync, readFileSync, appendFileSync, writeFileSync } from "node:fs";
import { resolve, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { execSync } from "node:child_process";

const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, "..");
const PUBLIC_DIR = resolve(ROOT, "public");
const ALL_PROJECTS = process.argv.includes("--all-projects");
const RESULTS_CSV = resolve(__dirname, "gemini-exposure-report.csv");

// Initialize CSV with header (overwrite any previous run)
writeFileSync(RESULTS_CSV, "project,key,models_status,files_status,status\n");

function saveResult(project, key, modelsStatus, filesStatus, exposed) {
  const row = `${project || "public/"},${key},${modelsStatus},${filesStatus},${exposed ? "EXPOSED" : "BLOCKED"}\n`;
  appendFileSync(RESULTS_CSV, row);
}

// -- gcloud helpers ------------------------------------------------------------
function gcloud(args) {
  try {
    return execSync(`gcloud ${args} 2>/dev/null`, { encoding: "utf-8" }).trim();
  } catch {
    return "";
  }
}

function listProjects() {
  const out = gcloud('projects list --format="value(projectId)"');
  return out ? out.split("\n").filter(Boolean) : [];
}

async function getFirebaseApiKeys(projectId) {
  const keys = new Set();

  // Approach 1: firebase apps:sdkconfig WEB (for projects with registered WEB apps)
  try {
    const raw = execSync(
      `firebase apps:list --project "${projectId}" --json 2>/dev/null`,
      { encoding: "utf-8" }
    );
    const parsed = JSON.parse(raw.slice(raw.indexOf("{")));
    const webAppIds = (parsed.result || [])
      .filter((a) => a.platform === "WEB")
      .map((a) => a.appId);
    for (const appId of webAppIds) {
      try {
        const configRaw = execSync(
          `firebase apps:sdkconfig WEB --project "${projectId}" "${appId}" --json 2>/dev/null`,
          { encoding: "utf-8" }
        );
        const configParsed = JSON.parse(configRaw.slice(configRaw.indexOf("{")));
        const apiKey = configParsed.result?.sdkConfig?.apiKey || configParsed.result?.apiKey;
        if (apiKey) keys.add(apiKey);
      } catch {}
    }
  } catch {}

  // Approach 2: /__/firebase/init.json (works even without registered apps)
  if (keys.size === 0) {
    try {
      const res = await fetch(
        `https://${projectId}.firebaseapp.com/__/firebase/init.json`,
        { signal: AbortSignal.timeout(6000) }
      );
      if (res.ok) {
        const json = await res.json();
        if (json.apiKey) keys.add(json.apiKey);
      }
    } catch {}
  }

  return [...keys];
}

// -- public/ scan helpers ------------------------------------------------------
function* walkJs(dir) {
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
    const full = resolve(dir, entry.name);
    if (entry.isDirectory()) yield* walkJs(full);
    else if (entry.isFile() && entry.name.endsWith(".js")) yield full;
  }
}

function extractKeys(filePath) {
  const content = readFileSync(filePath, "utf-8");
  return [...new Set(content.match(/AIzaSy[A-Za-z0-9_-]{33}/g) || [])];
}

// -- Test key against Gemini endpoints (TruffleHog PoC) -----------------------
async function testGeminiAccess(key) {
  const endpoints = [
    `https://generativelanguage.googleapis.com/v1beta/models?key=${key}`,
    `https://generativelanguage.googleapis.com/v1beta/files?key=${key}`,
  ];
  const results = await Promise.all(
    endpoints.map(async (url) => {
      const res = await fetch(url, { method: "GET" });
      return { url, status: res.status };
    })
  );
  return results;
}

// -- Build key map -------------------------------------------------------------
// Map: keyString -> { sources: string[], project?: string }
const keyMap = new Map();

function addKey(keyString, source, project) {
  if (!keyMap.has(keyString)) keyMap.set(keyString, { sources: [], project });
  keyMap.get(keyString).sources.push(source);
}

if (ALL_PROJECTS) {
  console.log("Enumerating all GCP projects via gcloud...");
  const projects = listProjects();
  console.log(`Found ${projects.length} project(s).\n`);
  for (const projectId of projects) {
    process.stdout.write(`  ${projectId}... `);
    const keys = await getFirebaseApiKeys(projectId);
    if (keys.length > 0) {
      console.log(`${keys.length} key(s) found`);
      for (const key of keys) addKey(key, `firebase/${projectId}`, projectId);
    } else {
      console.log(`no keys`);
    }
  }
  console.log();
} else {
  console.log("Scanning public/ for Firebase API keys...");
  for (const filePath of walkJs(PUBLIC_DIR)) {
    for (const key of extractKeys(filePath)) {
      addKey(key, filePath.replace(ROOT + "/", ""), undefined);
    }
  }
}

if (keyMap.size === 0) {
  console.log("No Firebase API keys found.");
  process.exit(0);
}

console.log(`Found ${keyMap.size} unique key(s). Testing against Gemini API...\n`);

let anyExposed = false;

for (const [key, { sources, project }] of keyMap) {
  const results = await testGeminiAccess(key);
  const exposed = results.some((r) => r.status === 200);
  const label = exposed ? "EXPOSED" : "BLOCKED";
  const projectTag = project ? ` [${project}]` : "";
  const [modelsResult, filesResult] = results;
  saveResult(project, key, modelsResult.status, filesResult.status, exposed);
  console.log(`${label}  ${key}${projectTag}`);
  console.log(
    `  Source: ${sources.slice(0, 3).join(", ")}${sources.length > 3 ? ` (+${sources.length - 3} more)` : ""}`
  );
  for (const { url, status: httpStatus } of results) {
    const endpoint = new URL(url).pathname;
    console.log(`  ${endpoint} -> HTTP ${httpStatus}`);
  }
  console.log();

  if (exposed) anyExposed = true;
}

if (anyExposed) {
  console.error("ACTION REQUIRED: At least one key is exposed to Gemini.");
  console.error("Steps:");
  console.error("  1. Go to Google Cloud Console -> APIs & Services -> Credentials");
  console.error(
    "  2. Find the exposed key and add apiTargets restriction (exclude generativelanguage.googleapis.com)"
  );
  console.error("  3. Also add HTTP referrer restrictions to limit key usage to your domains");
  console.error("  4. Re-run this script to confirm the fix");
  process.exit(1);
} else {
  console.log("All keys are blocked from Gemini. Project is safe.");
}

console.log(`\nResults saved to: ${RESULTS_CSV}`);

We received an unexpected bill for $13,000 USD on April 17th. We have only integrated Firebase Remote Templates for AI logic into our Android app, which is currently in its early testing phase with zero external users. During our tests, we generated only about 10 pieces of text and images. If this charge cannot be waived, we will be forced to shut down all services, terminate our relationship with Google, and dissolve our studio.

What was supposed to be Firebase AI Remote Templates feels more like ‘Remote Terminate Everything’. This unexpected $13,000 bill is effectively a kill switch for our business.

Hi @zanbezi — yours was the first case I read when I started realizing how deep our hole was.
We’re now staring at a $67k+ charge accrued in 19 hours, and if it gets enforced our small Korean startup doesn’t survive it.

The technical pattern matches yours closely (Firebase-provisioned key, Google’s own May 2024 restriction policy not applied) but honestly the part of your thread that hit hardest was just recognizing the exact shape of crisis we’re in right now. Full case documented in the post above.

If you’re open to it — what actually moved things from the initial denial to a real review on your end? Even one sentence from someone who’s lived through this would help.

Wishing you the best on your final resolution.

Google always said that these Firebase keys were fine to use in the browser. A decade later we enable Gemini for the project and then we got scammed for 3,5K USD . Seriously Google should refund it.

Hey @Logan_Kilpatrick,

Just following up regarding our €54k Gemini API billing case. I sent the details via email two weeks ago and followed up again a few days later, but wanted to check here as well in case it got lost.

Would really appreciate any guidance or if there is someone else on the team we should contact regarding a possible exception review.

Thanks again for offering to take a look.

Sorry to hear you are dealing with this as well. I sent you a DM, happy to chat further there, and wishing you and your team the best.