Il problema: quanto costa davvero far leggere un PDF a un'IA?
L'idea di partenza era semplice: un tool dove l'utente carica un PDF — un contratto, una relazione, un documento lungo — e riceve indietro un riassunto strutturato in pochi secondi. Il resto del cluster PDF del sito (unione, divisione, compressione, OCR) elabora già tutto lato client, a costo zero. Un riassunto con l'IA è diverso per natura: serve per forza una chiamata a un modello linguistico, e quella ha un costo reale per token. Prima di scrivere una riga di codice, la domanda vera era: quanto mi costa, a scala, se il tool viene usato davvero?
Con un modello economico come Gemini Flash-Lite i numeri sono più rassicuranti di quanto pensassi. Per un contratto di una ventina di pagine (circa 13.000 token di input più il prompt di istruzioni, e un riassunto di 500 token in output), il costo di una singola chiamata resta nell'ordine di pochi millesimi di dollaro — il grosso del costo arriva dal volume di input, tariffato molto meno del token di output. Anche documenti molto più lunghi restano sotto uno o due centesimi. Su base mensile, anche con un traffico sostenuto per un sito personale, la spesa resta contenuta a poche decine di euro nello scenario peggiore.
L'unico vero rischio di costo non è l'uso normale, ma l'abuso: qualcuno che manda in loop la chiamata o carica ripetutamente documenti enormi. Da qui la scelta di mettere un rate limit fin dal primo giorno, non come ottimizzazione successiva.
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.
Architettura: estrazione nel browser, elaborazione IA sul server
La decisione che ha guidato tutto il resto è stata tenere l'estrazione del testo lato client, riusando la stessa libreria di parsing PDF già presente nel cluster Estrai Testo, e mandare alla funzione serverless solo il testo pulito, mai il file binario.
| Livello | Scelta |
|---|---|
| Estrazione testo | PDF.js, interamente nel browser |
| Backend | Netlify Function, riceve solo testo |
| Modello IA | Gemini Flash-Lite via REST API |
| Rate limiting | Contatore per IP/giorno su Firestore, via Admin SDK |
| Storage documenti | Nessuno — elaborazione al volo, nulla viene salvato |
Il motivo principale non è solo di costo: le funzioni serverless hanno limiti stretti su dimensione del payload e tempo di esecuzione. Un PDF scansionato pesante rischierebbe di superarli facilmente. Estraendo il testo nel browser, alla funzione arriva un payload piccolo e prevedibile, il costo in token è più basso perché non c'è markup binario da processare, e il rischio di timeout scende quasi a zero. Per i PDF scansionati (immagine, non testo vero) resta disponibile come step preliminare opzionale l'OCR già presente nel cluster PDF.
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.
| Layer | Choice |
|---|---|
| Text extraction | PDF.js, entirely in the browser |
| Backend | Netlify Function, receives text only |
| AI model | Gemini Flash-Lite via REST API |
| Rate limiting | Per-IP/day counter on Firestore, via Admin SDK |
| Document storage | None — 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 senza obbligare al login
Non volevo mettere un muro di autenticazione davanti a un tool pensato per un uso rapido e occasionale. La soluzione è un contatore giornaliero per indirizzo IP, salvato lato server con le stesse credenziali di servizio già usate dalle altre funzioni del sito, verificato prima di ogni chiamata al modello e incrementato subito dopo. Superata una soglia giornaliera, la funzione risponde con un errore esplicito invece di chiamare l'IA.
Un dettaglio che mi ha fatto riflettere: le regole di sicurezza del database restano a allow read, write: if false per tutto il traffico diretto dal client, senza bisogno di aggiungere un'eccezione per la nuova collection del contatore. Le funzioni serverless usano l'Admin SDK con credenziali di servizio, che bypassa sempre le regole — quindi il "deny all" globale copre automaticamente anche una collection creata dopo, senza nessuna modifica.
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.
Il problema imprevisto: una chiave API con un formato mai visto prima
Arrivato al momento di collegare davvero la chiave API, ho ricevuto una chiave che iniziava con un prefisso diverso da quello con cui avevo sempre lavorato in progetti precedenti (compreso l'assistente IA integrato in un altro gestionale che gestisco). Il primo istinto è stato pensare a un errore — magari la copia sbagliata di un token OAuth invece di una vera API key.
Non era un errore: è il nuovo formato di "Auth Key" che le interfacce di creazione più recenti generano di default. Il problema reale, però, era tecnico: le chiavi nel nuovo formato non funzionano se passate come parametro ?key= nell'URL della richiesta — il metodo più comune nei tutorial e in codice già scritto — e restituiscono un errore di autenticazione. Vogliono invece un header HTTP dedicato.
Ho aggiornato la funzione per passare la chiave con l'header x-goog-api-key invece che nell'URL — compatibile sia con il nuovo formato che con quello precedente. Se un progetto smette improvvisamente di autenticarsi con Google dopo aver rigenerato una chiave, vale la pena controllare per prima cosa proprio questo.
Nota a margine di sicurezza operativa: durante il debug la chiave è stata incollata per errore in chiaro in una chat di lavoro. Anche in un canale privato, una chiave vista da qualcun altro (o da un altro sistema) va considerata potenzialmente compromessa: la prassi corretta è rigenerarla subito, non continuare a usarla "tanto nessuno l'ha vista".
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."
Verificare la chiave prima del deploy, non dopo
Invece di collegare subito la chiave rigenerata alla funzione in produzione, ho preparato uno script Node.js isolato che fa una singola chiamata di test al modello e stampa solo esito e nome del modello — mai la chiave stessa. Sul mio computer Windows, l'unico intoppo è stato ricordarsi che la sintassi per impostare una variabile d'ambiente in PowerShell è diversa da quella di bash: due comandi separati invece di una singola riga.
Una volta ottenuta conferma che la chiave funzionasse e che il nome del modello fosse quello effettivamente disponibile per l'account, il collegamento a Netlify come variabile d'ambiente riservata alle funzioni è stato l'ultimo passo, senza sorprese.
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.
Dalla pagina "solo form" a un tool indicizzabile
La prima versione della pagina del tool era, di fatto, quasi solo un modulo di caricamento file: funzionale per chi ci arriva già sapendo cosa fare, ma un problema per la ricerca organica. Una pagina con poco testo indicizzabile intorno al form viene classificata come "thin content" — Google non ha abbastanza segnali per capire di cosa tratta e per quali ricerche mostrarla.
Ho aggiunto: hreflang coerente con l'architettura bilingue a URL singolo del sito, dati strutturati BreadcrumbList e FAQPage (oltre al WebApplication già presente), una sezione "Come funziona" in tre passaggi, una FAQ visibile identica per contenuto ai dati strutturati, e link interni verso gli altri strumenti del cluster PDF. Tutto in versione italiana e inglese, validato prima della consegna: bilancio dei tag div, parsing dei tre blocchi JSON-LD, nessuna credenziale nel codice.
Un ultimo dettaglio quasi comico: dopo aver collegato il tool, mi sono accorto che non aveva lo sfondo animato canvas presente in tutte le altre pagine del sito. La causa era banale — nella pagina avevo usato un semplice <div id="canvas-container"> invece dell'elemento <canvas id="canvas"> che lo script di animazione condiviso cerca esplicitamente per id. Un promemoria di quanto un dettaglio piccolo, copiato male da un template, possa passare inosservato finché qualcuno non lo nota a occhio.
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.
Cosa mi porto a casa da questo progetto
La lezione più utile non è tecnica in senso stretto: è che un tool "gratis per l'utente" non è mai davvero a costo zero per chi lo mantiene, e vale la pena fare i conti prima di scrivere codice, non dopo. Nel mio caso i numeri hanno confermato che il progetto era sostenibile — ma il rate limiting resta comunque il primo pezzo che ho scritto, non l'ultimo. Lo stesso vale per la sicurezza operativa: una chiave vista per sbaglio da un occhio in più si rigenera, punto, senza calcolare quanto sia "probabile" che sia stata usata.
Se ti interessa il resto del cluster PDF costruito con lo stesso principio di elaborazione client-side, ne ho parlato anche a proposito della redazione reale dei PDF con PDF.js e pdf-lib. E se il tema è integrare l'IA di Google in un progetto più grande, ho raccontato il percorso completo nel assistente IA con Gemini dentro un gestionale.
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.
Domande frequenti
Quanto costa riassumere un PDF con l'IA usando Gemini?
Con un modello economico come Gemini Flash-Lite, un contratto di una ventina di pagine costa nell'ordine di pochi millesimi di dollaro a riassunto. Anche documenti da un centinaio di pagine restano sotto uno o due centesimi. A traffico sostenuto, la spesa mensile resta comunque contenuta a poche decine di euro.
Perché estrarre il testo del PDF nel browser invece di caricare il file alla funzione serverless?
Le funzioni serverless hanno limiti stretti su payload e tempo di esecuzione. Estraendo il testo lato client, alla funzione arriva solo testo pulito: meno byte, meno token da pagare, meno rischio di timeout, e il file binario non lascia mai il browser dell'utente.
Come si evita l'abuso di un tool IA gratuito esposto pubblicamente?
Con un contatore per indirizzo IP con reset giornaliero, salvato lato server con credenziali di servizio e verificato prima di ogni chiamata al modello. Le regole di sicurezza del database possono restare a "nega tutto" per il traffico diretto dal client.
Perché una chiave API Google può iniziare con un prefisso diverso dal solito?
Google sta migrando verso un nuovo formato di credenziale, generato di default dalle interfacce più recenti. Le chiavi nel nuovo formato vanno passate come header HTTP dedicato, non come parametro nell'URL — altrimenti l'autenticazione fallisce.
Perché una pagina-tool con solo un form si posiziona male su Google?
Senza testo indicizzabile intorno al form, la pagina viene classificata come "thin content". Una sezione di spiegazione, una FAQ visibile coerente con i dati strutturati e link interni verso strumenti correlati danno al motore di ricerca il contesto che il solo form non fornisce.
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.