The problem: what does it actually cost to have an AI read a PDF?

The starting idea was simple: a tool where the user uploads a PDF — a contract, a report, a long document — and gets back a structured summary in a few seconds. The rest of the site's PDF cluster (merge, split, compress, OCR) already processes everything client-side, at zero cost. An AI summary is different by nature: it necessarily needs a call to a language model, and that has a real per-token cost. Before writing a single line of code, the real question was: what does this cost me at scale, if the tool actually gets used?

With a cheap model like Gemini Flash-Lite, the numbers were more reassuring than I expected. For a twenty-page-ish contract (roughly 13,000 input tokens plus the instruction prompt, and a 500-token summary in output), a single call costs on the order of a few thousandths of a dollar — most of the cost comes from input volume, priced far lower than output tokens. Even much longer documents stay under one or two cents. On a monthly basis, even with sustained traffic for a personal site, the worst-case spend stays in the tens of euros.

💡

The real cost risk isn't normal use, it's abuse: someone looping the call or repeatedly uploading huge documents. That's why I built in a rate limit from day one, not as a later optimization.

Architecture: browser-side extraction, server-side AI processing

The decision that shaped everything else was to keep text extraction client-side, reusing the same PDF parsing library already used in the Extract Text tool, and to send the serverless function only clean text, never the binary file.

LayerChoice
Text extractionPDF.js, entirely in the browser
BackendNetlify Function, receives text only
AI modelGemini Flash-Lite via REST API
Rate limitingPer-IP/day counter on Firestore, via Admin SDK
Document storageNone — processed on the fly, nothing is saved

The main reason isn't only cost: serverless functions have tight limits on payload size and execution time. A heavy scanned PDF could easily blow past them. By extracting text in the browser, the function receives a small, predictable payload, the token cost is lower since there's no binary markup to process, and timeout risk drops close to zero. For scanned PDFs (image, not real text), the OCR already present in the PDF cluster remains available as an optional preliminary step.

Rate limiting without forcing a login

I didn't want to put a login wall in front of a tool meant for quick, occasional use. The solution is a per-IP daily counter, saved server-side with the same service credentials already used by the site's other functions, checked before every model call and incremented right after. Once a daily threshold is hit, the function returns an explicit error instead of calling the AI.

One detail that made me stop and think: the database security rules stay at allow read, write: if false for all direct client traffic, with no need to add an exception for the new counter collection. Serverless functions use the Admin SDK with service credentials, which always bypasses the rules — so the global "deny all" automatically covers a collection created later too, with no changes needed.

The unexpected snag: an API key in a format I'd never seen

When it came time to actually wire up the API key, I got one starting with a different prefix than what I'd always worked with in previous projects (including the AI assistant built into another business app I maintain). My first instinct was to assume a mistake — maybe an OAuth token copied by accident instead of a real API key.

It wasn't a mistake: it's the newer "Auth Key" format that the latest creation interfaces generate by default. The real problem, though, was technical: keys in the new format don't work when passed as a ?key= URL parameter — the most common method in tutorials and in already-written code — and return an authentication error. They expect a dedicated HTTP header instead.

⚠️

I updated the function to pass the key via the x-goog-api-key header instead of the URL — compatible with both the new and the previous format. If a project suddenly stops authenticating with Google after regenerating a key, this is the first thing worth checking.

A side note on operational security: during debugging the key was accidentally pasted in plain text into a work chat. Even in a private channel, a key seen by anyone else (or by another system) should be treated as potentially compromised: the correct move is to rotate it immediately, not to keep using it because "no one really saw it."

Verifying the key before deploy, not after

Instead of wiring the regenerated key straight into the production function, I prepared an isolated Node.js script that makes a single test call to the model and prints only the outcome and the model name — never the key itself. On my Windows machine, the only snag was remembering that the syntax for setting an environment variable in PowerShell differs from bash: two separate commands instead of one line.

Once I confirmed the key worked and that the model name was actually available for the account, wiring it into Netlify as a function-scoped environment variable was the last, uneventful step.

From a "form-only" page to an indexable tool

The first version of the tool page was, in practice, almost just a file upload form: functional for someone who already knows what to do, but a problem for organic search. A page with little indexable text around the form gets classified as "thin content" — Google doesn't have enough signal to understand what it's about or which searches to show it for.

I added: hreflang consistent with the site's single-URL bilingual architecture, structured data for BreadcrumbList and FAQPage (on top of the existing WebApplication), a three-step "How it works" section, a visible FAQ matching the structured data content, and internal links to the other tools in the PDF cluster. All in Italian and English, validated before delivery: div tag balance, parsing of all three JSON-LD blocks, no credentials in the code.

One almost comic detail: after wiring up the tool, I noticed it was missing the animated canvas background present on every other page of the site. The cause was trivial — I'd used a plain <div id="canvas-container"> in the page instead of the <canvas id="canvas"> element the shared animation script explicitly looks up by id. A reminder of how a small detail, copied wrong from a template, can go unnoticed until someone spots it by eye.

When this PDF summarization approach makes sense (and when it doesn't)

When to use it

  • Free public tool with high potential volume: the browser-extraction + server-side-AI-processing architecture minimizes cost per use, making a free service sustainable.
  • Rate limiting without forcing registration: a public tool requiring login for occasional use discourages users — rate limiting via Firestore solves this without friction.
  • Standard text documents (not pure image scans), where client-side text extraction with PDF.js works reliably before passing content to the model.

When not to use it

  • Scanned PDFs as images with no text layer: PDF.js extracts nothing from an image; an OCR step would be needed first, adding cost and complexity.
  • Extremely long documents: past a certain size, the per-call cost to the model grows and free rate limiting becomes less sustainable without a paid tier.
  • Maximum accuracy needed on critical legal or technical documents: an automatic summary should always be treated as a starting point, not a substitute for full reading on high-risk documents.

Alternatives

For editing rather than summarizing a PDF (redaction, signing, page rotation), see the PDF editor with real redaction, which tackles a complementary problem with different tools (pdf-lib instead of an AI model).

Common mistakes

  • Not verifying the API key format before deploy: keys with non-standard formats (like the AQ. prefix of some Gemini "Auth Key" credentials) require a different HTTP header than expected — a pre-deploy check avoids an error only discovered in production.
  • Designing a "form-only" tool with no indexable text content: a page that's just an interactive interface with no surrounding text loses the SEO opportunity to explain what the tool does and for whom.
  • Underestimating per-call cost when choosing the model: cheaper models like Gemini Flash-Lite are often enough for summaries, avoiding paying for capacity unnecessary for this use case.

What I take away from this project

The most useful lesson isn't strictly technical: a "free for the user" tool is never truly zero-cost for whoever maintains it, and it's worth doing the math before writing code, not after. In my case the numbers confirmed the project was sustainable — but rate limiting is still the first piece I wrote, not the last. The same goes for operational security: a key seen by an extra pair of eyes gets rotated, full stop, without calculating how "likely" it is that it was actually used.

If you're interested in the rest of the PDF cluster built on the same client-side processing principle, I also wrote about real PDF redaction with PDF.js and pdf-lib. And if the topic is integrating Google's AI into a larger project, I covered the full journey in the Gemini AI assistant built into a business app.

Frequently asked questions

How much does it cost to summarize a PDF with AI using Gemini?

With a cheap model like Gemini Flash-Lite, a twenty-page-ish contract costs on the order of a few thousandths of a dollar per summary. Even hundred-page documents stay under one or two cents. At sustained traffic, monthly spend stays in the tens of euros.

Why extract the PDF text in the browser instead of uploading the file to the serverless function?

Serverless functions have tight payload and execution-time limits. By extracting text client-side, the function receives only clean text: fewer bytes, fewer tokens to pay for, less timeout risk, and the binary file never leaves the user's browser.

How do you prevent abuse of a free AI tool exposed publicly?

With a per-IP counter that resets daily, saved server-side with service credentials and checked before every model call. Database security rules can stay at "deny all" for direct client traffic.

Why can a Google API key start with a different prefix than usual?

Google is migrating toward a newer credential format, generated by default in the latest creation interfaces. Keys in the new format need to be passed as a dedicated HTTP header, not a URL parameter — otherwise authentication fails.

Why does a form-only tool page rank poorly on Google?

Without indexable text around the form, the page gets classified as "thin content". An explanation section, a visible FAQ matching the structured data, and internal links to related tools give search engines the context the form alone doesn't provide.