Cada cerca semàntica d'un dels meus projectes costa diners: un embedding, una crida de reranking i una síntesi de text. Cèntims per petició. Multiplicat per un bot amb un bucle, no són cèntims. La primera versió no demanava compte per a res, perquè demanar compte per buscar anime semblava absurd. Ho continua semblant. Ho faig igualment.
La raó no és el màrqueting ni la llista de correu. És que sense identitat no hi ha res a comptar.
El cost de deixar entrar sense identificar
Vaig muntar un harness de validació fake-door amb cinc landings alhora per mesurar interès abans de construir res. L'única cosa que calia protegir era el panell d'admin, així que vaig posar bandejament d'IP després de diversos errors de login. Suficient, vaig pensar.
No ho era. La IP del client venia de x-forwarded-for, i aquesta capçalera és una llista client, proxy1, proxy2… on cada proxy afegeix a la dreta però ningú no neteja el que ja hi havia escrit. Agafar el primer valor és agafar el que el client vulgui escriure. Qualsevol envia x-forwarded-for: 1.2.3.4, esquiva el seu propi bandejament, i de passada pot bandejar la IP d'un altre escrivint la seva.
Ho vaig arreglar comptant salts en comptes de confiant en l'esquerra:
const TRUSTED_PROXY_HOPS = Number(process.env.TRUSTED_PROXY_HOPS || 1);
const list = String(xff).split(",").map((s) => s.trim()).filter(Boolean);
const idx = list.length - TRUSTED_PROXY_HOPS;
return list[idx >= 0 ? idx : 0];
Amb un proxy al davant, el client real és l'últim valor: el que escriu el proxy i el client no pot tocar. Funciona. Però fixa't en el que acabo de fer: he ficat una variable d'entorn que depèn de la topologia de xarxa del desplegament per poder identificar algú de manera aproximada. Si demà fico un CDN al davant, el número canvia i el sistema de bandejaments s'equivoca en silenci. No falla: s'equivoca.
Per què la IP no serveix com a identitat
A part de ser falsificable darrere del proxy equivocat, la IP és una mala clau per raons que no tenen arreglament. Quatre persones de la mateixa oficina en comparteixen una. Una persona al mòbil canvia d'IP en sortir del portal. Les xarxes mòbils fan NAT sobre blocs enormes: bandejar una IP d'operador és bandejar un barri sencer.
El que vols d'un identificador és que sigui estable per a la mateixa persona i diferent per a persones diferents. La IP no compleix cap de les dues. Un user_id compleix les dues per construcció.
El que només pots fer quan saps qui és qui
Al cercador vaig acabar amb tres nivells i un límit finit als tres:
visitor: { semanticSearchLimit: 10, historyLimit: 0 }
registered: { semanticSearchLimit: 30, historyLimit: 10 }
premium: { semanticSearchLimit: 200, historyLimit: null }
El comentari que vaig deixar al tipus és el criteri sencer: sempre finit; cap nivell no està exempt de la quota de cerca. El premium té el número més alt, no l'infinit. historyLimit sí que admet null per a "il·limitat". El de cerca no, perquè cada cerca és una factura.
El visitant continua existint, amb una galeta que li dona una clau anònima. Però la finestra de renovació és diferent segons qui siguis: el registrat renova per mes natural en UTC, el premium renova a l'aniversari de la seva alta. Això exigeix desar un premiumAnchorAt, i desar qualsevol cosa exigeix una fila amb amo. Sense compte no hi ha aniversari per ancorar.
El cas de les apps amb cost per petició
Quan cada acció costa diners reals, la quota deixa de ser una defensa i passa a ser comptabilitat. I la comptabilitat falla de maneres avorrides.
Al cercador, dues peticions idèntiques es comptaven dues vegades. La solució va ser una reserva amb hash del contingut de la petició, no del seu text: la query normalitzada, el límit, el flag d'explícit i els filtres ordenats. El comentari del tipus diu per què l'ordre importa — el mateix conjunt triat en un altre ordre és la mateixa cerca — i per què els filtres van a dins: el mateix text amb un altre gènere és una altra petició, i fondre'ls tornaria resultats equivocats. La reserva torna reserved, duplicate, in_progress o exhausted. Els quatre estats existeixen perquè els quatre van passar.
En una API de transcripció, el cobrament per caràcters va començar sent transcripció més text processat dividit entre deu. En mesurar el cost real, la línia va passar a això:
transcriptionText.length + Math.floor(processedText.length * 0.25)
De /10 a * 0.25: dues vegades i mitja més car. El preu estava malament per un factor de 2,5 i durant mesos vaig estar pagant la diferència sense saber-ho, perquè no tenia el mesurament per adonar-me'n.
En un SaaS de generació d'imatges, el bug va ser el contrari: els crèdits diaris existien però la comprovació prèvia només mirava subCredits. Els usuaris amb crèdit diari rebien un 402 amb crèdit al compte. Un dailyCredits + subCredits ho va arreglar. Tot això són columnes d'una fila d'usuari. Sense fila, no hi ha bug per arreglar perquè no hi ha res a comptar.
Quan la identitat tampoc no basta
Tenir compte no és tenir identitat vàlida. En una API amb claus, la fila de la key sobrevivia a l'estat del compte de l'amo: es netejava el registre de facturació a l'offboarding sense que ningú toqués l'isActive de la key. La key continuava autenticant. L'arreglament va ser revalidar al mateix authenticateApiKey que l'amo continua tenint registre de facturació, en comptes de refiar-se del flag:
if (!ownerBilling) {
return { authenticated: false, error: "API key owner is no longer active" };
}
Va allà i no a cada endpoint, precisament perquè cap endpoint no se n'hagi de recordar.
Quan sí que deixo entrar sense compte
Quan l'acció no costa res i no acumula estat: llegir una fitxa, veure un catàleg, carregar una landing. I un nivell de visitant amb quota curta, perquè algú pugui provar abans de decidir. Deu cerques abans del registre és fricció honesta; zero és una factura oberta.
La pregunta no és si el login molesta. Molesta. La pregunta és què escriuràs al WHERE quan algú es passi de la ratlla.
