Motor de lead-uri B2B (date firme RO)
Un motor complet de inteligență comercială B2B pentru piața românească, construit integral solo, în Python cu orchestrare LLM (Claude Code + DeepSeek). Transformă registrul public de firme într-un flux zilnic de lead-uri pre-calificate.
Trage din 16+ surse de date (ANAF prin API cu 3 endpoint-uri, ONRC, mfinanțe, VIES, risco.ro, TERMENE, listafirme, Google Maps & Business Profile, BuiltWith, Hunter/Apollo, OpenCorporates, Clay, Similarweb, Semrush, DataForSeo), le îmbogățește, le punctează 0-100 după cifra de afaceri estimată, potrivire și accesibilitate, apoi le califică automat.
Rezultat: 8.307 firme la 99% acoperire de date, throughput de 10.000 firme/zi pe un singur cont de proxy și 1.000 de lead-uri pre-calificate pe zi în CRM, un proces manual (Excel + verificări pe Google și agenți AI) transformat într-un pipeline automat.
Citește studiul de caz complet →
Problema
Când am preluat prospectarea, totul trăia într-un singur fișier Excel partajat. Fiecare firmă era o linie, iar calificarea se făcea de mână, rând cu rând. Îmbogățirea datelor era complet manuală: deschideam fiecare firmă pe Google și pe câteva asistențe AI de chat, ciuguleam ce găseam, un număr de telefon aici, un site acolo, o impresie despre activitate, și lipeam totul înapoi în spreadsheet. Nu existau scripturi, nici API-uri de registru, nici acces la ANAF/ONRC, și nici o automatizare. De la momentul în care o firmă apărea ca posibilă țintă până când era efectiv calificată trecea aproximativ o săptămână de muncă de mână.
Ca să înțelegem de ce era atât de lent, merită descompus pas cu pas ce însemna „de mână”. Pentru o singură firmă, fluxul arăta așa: căutam denumirea pe Google, deschideam primele rezultate ca să confirm că firma există și că mai funcționează, întrebam una sau două asistențe AI de chat ca să adun context, copiam manual datele de contact, apoi mă întorceam în Excel și completam celulele. Fiecare dintre acești pași era un punct în care se putea strecura o eroare de transcriere sau o informație învechită. Nu exista nicio sursă unică de adevăr, doar ce reușeam să adun într-o fereastră de browser și să lipesc înapoi.
Procesul era corect, datele pe care le adunam erau reale, dar pur și simplu nu se putea scala. O singură persoană putea procesa câteva zeci de firme înainte ca decalajul de aproximativ o săptămână să transforme orice listă într-un set de date învechite. Și învechirea nu era o problemă teoretică: o firmă radiată între momentul în care o adăugam în listă și momentul în care ajungeam la ea rămânea acolo, arătând ca o țintă validă când de fapt nu mai exista. Contactele lipseau pentru că nu aveam o cale sistematică de a le găsi; dacă nu apărea un număr în primele rezultate Google, rândul rămânea gol. Iar deciziile despre care firmă merita abordată se luau pe baza unei impresii, nu pe semnale verificate.
Problema de fond era că volumul și calitatea se băteau cap în cap. Dacă încercam să procesez mai multe firme pe zi, scădeam timpul alocat fiecăreia și calitatea verificării scădea odată cu el. Dacă verificam temeinic fiecare firmă, numărul de firme pe zi se prăbușea. Nu exista nicio cale, în modelul manual, să cresc ambele în același timp. Excelul partajat amplifica problema: fiind un singur fișier editat de mână, nu avea un istoric de stare al fiecărei firme, nu putea marca automat o firmă ca radiată și nici reverifica nimic. Era o fotografie statică a unui moment care îmbătrânea din clipa în care o salvai.
Acel decalaj, săptămâna dintre momentul în care o firmă apărea ca posibilă țintă și momentul în care era efectiv calificată, este exact motivul pentru care am construit pipeline-ul. Nu plecam de la dorința de a folosi tehnologie de dragul tehnologiei; plecam de la o constrângere foarte concretă: o singură persoană, un singur Excel, o săptămână de întârziere și o listă care se învechea mai repede decât o puteam curăța. Fiecare decizie de inginerie care a urmat a fost o reacție directă la una dintre aceste limite. Lipsa unei surse de adevăr a cerut interogarea directă a registrelor oficiale, în loc de ciugulit din rezultate Google. Munca manuală repetată pe fiecare firmă a cerut automatizare care să ruleze fără supraveghere. Decalajul de o săptămână a cerut un sistem capabil să macine mii de firme pe zi, nu câteva zeci.
Mi-am propus, așadar, un obiectiv simplu de formulat și greu de atins: să transform un proces manual de o săptămână într-un sistem care macină mii de firme pe zi, complet nesupravegheat. „Nesupravegheat” era partea importantă; nu voiam o unealtă care doar accelera munca de mână, ci una care să o înlocuiască, rulând singură, verificând singură statusul firmelor și livrând semnale pe care să pot avea încredere fără să le reconfirm manual. Restul poveștii este modul în care am ajuns de la spreadsheet-ul acela partajat la pipeline-ul automat care a înlocuit fiecare pas descris mai sus.
Sursele de date
Pipeline-ul consumă peste 16 surse, dar nu sunt egale ca greutate. Coloana vertebrală este ANAF, și subliniez asta dintr-un motiv de inginerie: îl consum în principal prin API-ul lui live, nu doar prin descărcarea în bloc. Diferența contează, fiindcă un export în bloc e o fotografie de ieri, în timp ce un apel API îți dă starea de acum, exact ce trebuie când decizi dacă o firmă merită contactată azi. Sunt trei endpoint-uri live, fiecare ales pentru un rol precis:
- Identitate / registru TVA: serviciul web v9 PlatitorTvaRest. Trimit un array JSON de CUI-uri (până la 500 per apel), gratuit, fără autentificare. Batching-ul de 500 nu e cosmetic: transformă o problemă de O(n) apeluri într-una de O(n/500), deci tot universul de firme se interoghează în câteva sute de cereri în loc de sute de mii. Pentru fiecare firmă primesc denumirea, numărul de la registrul comerțului, adresa, telefon (circa 71% populat), data înregistrării, statusul de plătitor TVA, statusul activ/inactiv, datele de inactivare/reactivare/radiare, forma juridică și codul CAEN, plus semnale recente precum TVA la încasare, Split-TVA și e-Factura. Acesta este pasul 1 de calificare fiscală și, în același timp, un filtru preflight pentru firme radiate/inactive: dacă le elimin aici, nu mai cheltuiesc apeluri scumpe pe ele în pașii următori. Filtrul eșuează permisiv: dacă API-ul dă eroare, firma e tratată ca activă, fiindcă prefer un fals pozitiv (o firmă moartă care trece) unei firme bune pierdute dintr-un timeout de rețea.
- Situații financiare (bilanț): endpoint GET, o firmă-an per cerere, fără batching. Aici nu am de ales: ANAF expune bilanțurile doar individual. Limita de 1 cerere/secundă e respectată strict, iar pe reset de conexiune reîncerc o dată cu o pauză de 2 secunde, suficient cât conexiunea să se așeze fără să irosesc timp. Necesită imitarea amprentei TLS Chrome120, altfel ANAF refuză conexiunea la nivel de handshake, înainte de orice HTTP; e un detaliu care, dacă îl ratezi, îți pare că serverul e căzut când de fapt te respinge ca bot. Mapez 20 de indicatori (I1..I20) pe câmpuri: cifra de afaceri, profit net, pierdere netă, număr de salariați, datorii și așa mai departe, exact materia primă pentru scorul de mărime și sănătate al firmei.
- Batch pentru numărul de registru (J): același serviciu v9, dar condus în mod batch (500 CUI per cerere, răspuns 200-500ms) special pentru a recupera numărul de la registrul comerțului împreună cu statusul fiscal. E o a doua treabă a aceluiași endpoint, separată logic fiindcă numărul J leagă firma de ONRC.
Pe lângă cele trei API-uri live, există o descărcare opțională în bloc de la ANAF (CKAN data.gov.ro): patru fișiere care însumează universul complet de firme. Bulk-ul e util ca să ai lista de pornire fără mii de apeluri, dar modificările de status (e-Factura, Split-TVA, data inactivării) NU sunt în CSV-urile în bloc. Asta confirmă de ce ANAF se consumă în principal prin API-ul live, cu bulk-ul doar ca supliment: bulk-ul îți dă cine există, API-ul live îți dă cum stau lucrurile acum.
Restul surselor acoperă goluri specifice. ONRC (API-ul live Recom prin myportal plus CSV-uri lunare în bloc de la data.gov.ro: registrul de firme și fișierul de reprezentanți legali de 333 MB) leagă firma de oameni și de actul oficial. VIES validează TVA intracomunitar prin SOAP, util pentru firme care exportă. BPI și dosarele de instanță vin prin serviciul SOAP public ECRIS (portal.just.ro), căutând după numele părții: semnale de litigiu și insolvență. risco.ro aduce risc și date financiare (plătit); TERMENE acoperă achiziții și instanță (plătit, maxim 75 firme per interogare, motiv pentru care folosesc un algoritm de bracketing pe cifra de afaceri ca să împart universul în intervale care încap sub plafon). listafirme.ro servește descoperirea site-urilor; firme.info/firmeapi.ro dau datorii ANAF și insolvență; iar pentru stack web și SEO folosesc BuiltWith, SimilarWeb, Semrush, DataForSeo și Google Custom Search. Google Maps / Google Business Profile sunt surse live active, folosite pentru prezența locală și profilul de afacere; OpenCorporates rămâne o sursă activă pentru date corporative internaționale. Toată această colecție e ținută sub un singur cont de proxy BrightData, reglat la circa 10.000 de firme pe zi.
Ingestia
Consider că ingestia este stratul în care datele brute, eterogene, devin un flux organizat. Principiul de bază este simplu: fiecare sursă are propriul client, dar toate respectă aceeași disciplină. Această separare nu este accidentală; fiecare portal public are propriul comportament, propriile anteturi așteptate și propriul mod de a eșua, așa că a forța toate sursele într-un singur client generic ar fi însemnat fragilitate. În schimb, un client per sursă izolează ciudățeniile fiecăreia, iar disciplina comună (timeout-urile, reîncercările și formatul de ieșire) rămâne uniformă.
Am ales ca transportul implicit să fie curl_cffi cu imitarea amprentei TLS Chrome120, cu cădere pe requests/urllib din biblioteca standard. Motivul pentru care amprenta TLS contează este că multe servere inspectează handshake-ul TLS (ordinea cipherurilor, extensiile) pentru a distinge un browser real de un client automatizat; un client Python obișnuit are o amprentă caracteristică și ușor de respins. Prin imitarea exactă a amprentei Chrome120, conexiunea arată ca un browser legitim. Pentru bilanțul ANAF, această amprentă TLS Chrome120 nu este opțională: fără ea, ANAF refuză conexiunea. Căderea pe requests/urllib din biblioteca standard există ca plasă de siguranță: dacă biblioteca de imitare lipsește sau eșuează într-un mediu, pipeline-ul nu se oprește, ci continuă cu un transport mai simplu.
Consider că odată aduse, datele se adună într-un agregator per firmă. Aceasta este alegerea mea de design care ține totul coerent: indiferent din câte surse vine informația, ea converge spre un singur loc, indexat după firmă. Runnerul de îmbogățire v8 rulează o mașină de stare idempotentă peste pașii FETCHED → CUI_VERIFIED → ANAF_DONE → RISCO_DONE → QUALIFIED → PUSHED. Idempotența este aici garanția operațională centrală: dacă un pas este deja atins, este no-op. Concret, aceasta înseamnă că dacă runnerul se repornește după o cădere de rețea la jumătatea unei firme, repornirea nu reface pașii deja finalizați și nu dublează datele. O eroare de mașină nu poate corupe o firmă parțială; se reia din checkpoint. Această proprietate transformă un proces fragil, care altădată trebuia supravegheat manual, într-unul care se poate relua singur de oriunde a fost întrerupt.
Am configurat ca mergeul propriu-zis să citească fiecare JSON per sursă pentru un CUI normalizat și să scrie o singură înregistrare canonică. Normalizarea CUI-ului este pasul tăcut, dar critic: același cod fiscal poate apărea cu prefixul RO, cu spații sau cu zerouri inițiale diferite în surse diferite, iar fără o formă canonică aceeași firmă s-ar fragmenta în mai multe înregistrări. Scrierea este atomică: se scrie într-un fișier temporar, apoi se face replace, astfel încât un cititor nu vede niciodată o înregistrare pe jumătate scrisă, iar o întrerupere în timpul scrierii lasă întotdeauna fie vechea înregistrare intactă, fie pe cea nouă completă, niciodată ceva corupt la mijloc.
Am cablat nouă surse în agregatorul principal: identitate ANAF, financiare ANAF, administratori, firme.info, listafirme, Google Knowledge Panel, VIES, trafic și scorul de lead. Fiecare contribuie cu o fațetă diferită a profilului firmei: identitatea și statutul fiscal, situația financiară, persoanele de conducere, prezența online și semnalele de calitate ale leadului, iar combinația lor dă o imagine mult mai bogată decât ar putea oferi orice sursă singură. Pe lângă acestea, am configurat un al doilea agregator de semnale care unește șase surse transfrontaliere și aplică regexuri proaspete pentru curieri internaționali (DHL, FedEx, UPS, DPD, GLS, TNT, Aramex), selectoare de țară și monede/limbi non-RO, ca să scoreze intenția transfrontalieră. Logica este intuitivă: o firmă care menționează curieri internaționali, afișează prețuri în alte monede decât RON sau oferă conținut în limbi non-RO semnalează o activitate care depășește granițele țării, exact tipul de intenție pe care un operator B2B transfrontalier vrea să-l detecteze. Regexurile proaspete înseamnă că aceste tipare sunt aplicate la fiecare rulare pe datele curente, nu moștenite dintr-un cache vechi.
La nivel financiar, am configurat ca mergeul să prefere cifra de afaceri din API-ul live ANAF și să cadă pe revenue-ul parsat din firme.info doar când ANAF nu are nimic. Această ierarhie de încredere este deliberată: API-ul live ANAF este sursa autoritară, oficială, așa că primește mereu prioritate; firme.info funcționează strict ca rezervă, folosită numai când sursa autoritară tace. Astfel se evită situația în care o cifră mai puțin sigură suprascrie una oficială, iar datele financiare rămân cât se poate de aproape de adevărul oficial.
Anti-bot și scalarea la circa 10.000/zi pe un singur proxy
Toată scalarea s-a făcut pe un SINGUR cont de proxy rezidențial BrightData. Decizia de a nu cumpăra mai multe conturi a fost deliberată: un singur cont înseamnă o singură suprafață de risc de reputație, costuri previzibile și o disciplină forțată; dacă nu poți cumpăra lățime de bandă în plus, ești obligat să o folosești inteligent. La început am rulat un throttling foarte conservator per firmă, pentru că primul obiectiv nu era viteza, ci să nu ard contul înainte să înțeleg cum reacționează fiecare sursă. Pe măsură ce am înțeles pragurile, am reglat sistemul ca să susțină circa 10.000 de firme/zi pe acel cont unic (circa mai 2026).
Vreau să fiu corect cu cifrele, pentru că aici se ascund cele mai multe exagerări din prezentări: cei 10.000/zi sunt PLAFONUL configurat (DAILY_CAP), nu o medie observată. Throughput-ul realist per cale este circa 7.200-8.700/zi, iar o parte importantă din acest volum este dusă de scraping gratuit pe IP-ul de acasă, nu de proxy-ul plătit. Cu alte cuvinte, plafonul protejează contul de o zi anormală, iar IP-ul rezidențial propriu absoarbe cererile „ieftine” ca să păstreze bugetul de proxy pentru sursele cu adevărat dificile.
Arsenalul anti-bot, complet, și de ce arată așa:
- Imitarea amprentei TLS Chrome120 cu curl_cffi (JA3 + HTTP/2), nu doar string-uri de User-Agent. Mulți cred că este suficient să schimbi User-Agent-ul, dar firewall-urile moderne citesc handshake-ul TLS: ordinea cipher-elor, extensiile, comportamentul HTTP/2. Dacă antetul spune „Chrome”, dar amprenta JA3 strigă „librărie Python”, ești deja marcat. Calea riscantă rotește un pool de amprente [chrome120, chrome119, safari17_0, edge99], ca să nu existe o singură semnătură repetată la infinit.
- O scară de fetch pe 5 niveluri, de la ieftin la scump: curl_cffi, subprocess curl HTTP/1.1, downgrade HTTPS către HTTP, Playwright stealth, și BrightData Scraping Browser peste CDP. Logica este simplă: încerci întâi metoda care costă zero și este cea mai rapidă; escaladezi către browser real abia când chiar este nevoie, pentru că un browser headless consumă de zeci de ori mai multe resurse decât un fetch.
- Detecție de stub Cloudflare: interstitialele „Just a moment” care vin cu HTTP-200 sunt capcana clasică; pagina pare bună (cod 200), dar conținutul este gol. Acestea sunt prinse și forțează escaladarea la browser-ul stealth pe Playwright; acesta este exact momentul „ocolit Cloudflare prin browser stealth”.
- Limitare client-side prin file-mtime token-bucket: un lock global per-sursă peste toți workerii paraleli, fără server central. Folosirea timestamp-ului unui fișier ca „token-bucket” înseamnă că zeci de procese paralele își coordonează ritmul fără o coadă Redis sau un serviciu dedicat, ceea ce înseamnă mai puține piese care se pot strica.
- Un singur cont de proxy, cu rotație IP per-cerere (sticky a fost adăugat, apoi scos, pentru că păstrarea aceluiași IP creștea riscul de a fi corelat). Failsafe-urile sondează IP-ul de ieșire la începutul și finalul fiecărui batch, iar un audit zilnic numără IP-urile unice; dacă rotația se blochează pe un singur IP, vezi imediat în raport.
- Rotație de User-Agent pe 8 intrări, ca strat suplimentar peste amprenta TLS.
- Spawn-uri windowless CREATE_NO_WINDOW, ca să nu apară ferestre de consolă pe ecran și, mai important, ca să nu rămână scrapere orfane care consumă resurse după o cădere.
- Umanizare: jitter log-normal (pauze cu distribuție reală, nu intervale fixe care trădează un bot), navigare-momeală la fiecare câteva firme, încălzire de cookie de 60s la start de fereastră și backoff adaptiv pe rata de eșec; dacă eșecurile cresc, sistemul încetinește singur.
- Caching de stare a sesiunii în JSON cu re-login automat validat de un canar (o cerere-test care confirmă că sesiunea chiar este validă, nu doar prezentă); un daemon de refresh sondează la fiecare 30 de minute și reîmprospătează atomic starea de stocare, ca să nu prinzi o sesiune pe jumătate scrisă.
- Backoff de 1h pe 3 eșecuri consecutive, fișier STOP grațios (oprești curat, nu cu kill brutal), lock PID single-instance, verificări de spațiu liber pe disc înainte de batch, rotație de snapshot la fiecare 12 batch-uri și refresh de autentificare la fiecare 6 batch-uri.
Toate aceste straturi nu au apărut dintr-o dată. Banul din 01/05/2026, după doar 273 de cereri în 5,5 ore, este cel care a declanșat reproiectarea completă a limitării de rată. Acel incident a fost lecția: nu numărul de cereri conta, ci tiparul lor prea regulat. De acolo a venit umanizarea, jitter-ul log-normal și backoff-ul adaptiv.
Îmbogățire, scor și calificare
În etapa de îmbogățire și scor, transformăm datele brute din mai multe surse într-o listă de priorități pe care un om de vânzări o parcurge de sus în jos. Fiecare lead primește un scor 0-100, iar logica nu este o estimare intuitivă, ci un model canonic ponderat, calculat identic pentru fiecare firmă: scor = 0,30 × revenue (scala log) + 0,30 × procent internațional + 0,15 × platformă + 0,15 × recența + 0,10 × VIES verificat. Ponderile nu sunt arbitrare: cei doi factori care prezic cel mai bine dacă o firmă are nevoie reală de transport transfrontalier, anume mărimea (revenue) și gradul de expunere internațională, primesc fiecare 30%, iar semnalele de susținere (platformă, recența, VIES) completează restul.
- Revenue: scorul cifrei de afaceri ESTIMATE pe o scară logaritmică (1 mil. RON → 0, 50 mil. RON → 100). Am ales scala log pentru că diferența dintre o firmă de 1 mil. și una de 10 mil. contează enorm pentru calificare, în timp ce diferența dintre 40M și 50M aproape nu mai schimbă decizia; un model liniar ar strivi toate firmele mici într-o singură grupă. Când revenue lipsește, modelul nu renunță la lead, ci cade elegant pe numărul de salariați ca proxy (1 → 0, 100 → circa 60, 1000+ → 100), deoarece dimensiunea echipei corelează rezonabil cu volumul de activitate.
- Procentul internațional este liniar. Aici relația este directă: cu cât o firmă vinde mai mult în afara, cu atât mai probabil are nevoie recurentă de un operator transfrontalier, deci o creștere liniară reflectă fidel creșterea de oportunitate.
- Platformă: acordăm 100 pentru Shopify/WooCommerce/Magento/BigCommerce, 60 pentru custom/WordPress/Wix, altfel 30. Raționamentul: o platformă de e-commerce matură semnalizează un flux real de comenzi care trebuie livrate, în timp ce un site custom sau de prezentare lasă mai multă incertitudine, iar absența oricărui semnal de magazin coboară firma la prag minim.
- Recența: 70 dacă firma este activă la ANAF, plus 30 dacă există un procesator de plăți internațional (Stripe/PayPal). Statusul activ la ANAF, consumat în principal prin API-ul live cu 3 endpoint-uri al ANAF, confirmă că firma există și funcționează acum; prezența unui procesator internațional este dovada concretă că încasează deja bani din afara granițelor.
- VIES: 100 dacă este verificat, 0 dacă nu, 30 dacă este necunoscut (beneficiul îndoielii). Am adăugat această componentă explicit pentru că firmele verificate VIES sunt viabile pentru transport intra-UE: un cod valid în VIES înseamnă că firma poate factura intracomunitar fără TVA, deci este pregătită operațional pentru livrări în UE. Scorul de 30 pentru „necunoscut” evită penalizarea unei firme doar pentru că verificarea nu a returnat încă un răspuns.
Odată calculat scorul, grupăm leadurile în tieruri care ghidează efortul de vânzări: ≥80 hot, ≥60 warm, ≥40 cold, restul frozen. Astfel, omul de vânzări atacă întâi zona „hot”, unde probabilitatea de conversie este cea mai mare per minut investit.
Scorul singur nu este însă suficient; o firmă poate avea scor mare și totuși să fie necalificabilă. De aceea, porțile de calificare funcționează ca o singură sursă de adevăr care colectează TOATE condițiile, fără să scurt-circuiteze la prima regulă îndeplinită, și rezolvă verdictul prin precedență clară: REJECTED > PENDING > LEADS. Această ordine înseamnă că orice motiv de respingere are întâietate, iar un lead nu trece niciodată mai departe doar pentru că a bifat o condiție pozitivă, dacă există în paralel un motiv de blocare. Concret, respingem dur firmele radiate (RADIATA) și formele juridice non-RO (GMBH/LTD/INC/LLC/BV/OY), deoarece o firmă ștearsă din registru nu mai poate cumpăra nimic, iar o entitate străină nu aparține pieței țintă românești. Punem PENDING când lipsește email-ul sau administratorul, un lead pe care nu avem cum să-l contactăm nu este încă acționabil, dar nici nu merită aruncat: starea PENDING îl trimite înapoi la reîmbogățire, pentru ca pipeline-ul să încerce din nou să completeze datele lipsă la următoarea trecere. În fine, marcăm ca LEADS doar firmele pentru care avem date de contact utilizabile, adică email, telefon sau website, adică atunci când omul de vânzări are tot ce-i trebuie ca să sune o persoană reală, la o firmă reală, cu identificare fiscală validă. Pipeline-ul a contribuit astfel la transformarea unei liste brute într-un flux ordonat, în care fiecare lead ajuns la telefon este deja verificat, scorat și pregătit.
Deduplicare și integritate
Într-un proiect de lead-generation B2B la scară națională, același nume de firmă apare în zeci de variante: cu și fără „SRL”, cu diacritice sau fără, scris cu majuscule sau litere mici, prescurtat sau complet. Dacă te bazezi pe nume pentru a identifica o firmă, ajungi inevitabil să numeri același client de trei-patru ori, iar un pipeline care duplică firmele își pierde rapid credibilitatea în fața echipei de vânzări. De aceea, cheia de identitate nu este niciodată numele, ci CUI-ul (codul unic de identificare fiscală), pe care îl normalizez la 6-10 cifre. Normalizarea înseamnă că elimin prefixul „RO”, spațiile și zerourile de umplere, reducând orice formă de scriere la același șir de cifre – astfel, „RO XXXXXXXX”, „XXXXXXXX” și „roXXXXXXXX” devin toate aceeași cheie canonică.
Pe această cheie rulează deduplicarea. Când aceeași firmă este văzută din mai multe surse – API-ul live cu 3 endpoint-uri al ANAF pentru datele fiscale, Google Maps și Google Business Profile pentru prezența locală și datele de contact, iar OpenCorporates pentru date corporative – fiecare aduce o bucată diferită de adevăr. ANAF știe dacă firma este plătitoare de TVA și dacă este activă, Google Maps oferă adresa fizică, programul și recenziile, iar OpenCorporates aduce structura legală și istoricul. Logica de merge nu alege arbitrar un câștigător, ci păstrează înregistrarea cea mai bogată: atunci când două intrări au același CUI normalizat, ele se contopesc într-o singură înregistrare canonică, acumulând câmpurile cele mai complete din fiecare sursă, în loc să se suprascrie reciproc și să piardă informație.
Această colapsare are două proprietăți pe care le-am căutat intenționat. Prima este zero duplicate: indiferent câte surse văd aceeași firmă, în ieșirea finală există o singură linie per CUI, astfel că numărul de leaduri raportat este numărul real de companii distincte, nu o cifră umflată. A doua este lipsa curselor de scriere. Deoarece pipeline-ul procesează până la circa 10.000 de firme pe zi printr-un singur cont de proxy BrightData, reglat fin pe acest debit, mai multe fluxuri pot atinge aceeași înregistrare aproape simultan. Dacă scrierile s-ar suprapune direct peste fișierul de date, două actualizări concurente s-ar putea călca una pe alta și ar lăsa o înregistrare coruptă, pe jumătate scrisă. Soluția este merge-ul atomic „temporar-apoi-replace”: noua versiune a datelor se scrie integral într-un fișier temporar, iar abia după ce scrierea s-a încheiat complet, acel fișier înlocuiește, printr-o singură operație de rename, versiunea veche. Rename-ul este atomic la nivel de sistem de fișiere, așa că un cititor vede fie starea veche completă, fie starea nouă completă – niciodată un amestec parțial.
Peste această integritate structurală se așază un strat de integritate probatorie. Fiecare înregistrare poartă hash-uri de audit SHA-256, calculate pe conținutul intrat. Rolul lor nu este criptografic în sens de securitate, ci de trasabilitate: pot dovedi exact ce date au intrat în sistem și din ce sursă au venit. Dacă cineva întreabă de ce o firmă are o anumită adresă sau de ce a fost marcată ca activă, hash-ul ancorează acel câmp la snapshot-ul exact de la care a fost preluat. Când o sursă se reactualizează și valoarea unui câmp se schimbă, hash-ul diferă de cel anterior, semnalând că înregistrarea s-a modificat – fără să fie nevoie să compar manual fiecare câmp. Asta transformă un simplu tabel de leaduri într-un dataset auditabil, în care fiecare afirmație despre o firmă poate fi urmărită până la origine.
Punctul de plecare făcea aceste garanții imposibile. Înainte, verificarea firmelor se făcea în Excel, completat manual cu căutări pe Google și întrebări adresate unui chat AI, cu un decalaj de aproximativ o săptămână între momentul în care o firmă intra în atenție și momentul în care datele ei erau confirmate. În Excel nu exista o cheie de identitate impusă, așa că aceeași firmă apărea pe rânduri diferite fără ca cineva să observe; nu exista nicio garanție de atomicitate, deci o salvare întreruptă strica fișierul; și nu exista niciun fel de hash de audit, deci era imposibil de spus retroactiv de unde venea o anumită valoare. Combinația dintre CUI normalizat ca cheie, merge-ul atomic temporar-apoi-replace și hash-urile SHA-256 înlocuiește acea fragilitate cu un proces în care aceeași firmă, văzută din mai multe surse, colapsează într-o singură înregistrare canonică, fără duplicate și fără curse de scriere – o bază pe care echipa de vânzări poate avea încredere fără să verifice manual fiecare linie.
Rulare neasistată (DuckDB / WAL / daemon)
Dacă primele etape ale pipeline-ului răspund la întrebarea "ce date adun și cum le calific", această etapă răspunde la întrebarea mult mai grea: "cum țin asta în viață zile la rând, fără ca eu să stau cu mâna pe el". Punctul de plecare era un proces manual de o săptămână per firmă, cu îmbogățire făcută de mână din Google și din câteva asistente AI de chat, într-un Excel partajat. Ca să îl transform în infrastructură care rulează singură, am avut nevoie de trei lucruri: o stocare pe care o pot relua după orice cădere, un model de scriere fără curse și un singur proces care orchestrează totul fără supraveghetor uman.
Stocarea este DuckDB, cu un INSERT-OR-REPLACE atomic pe cheia primară CUI. Alegerea CUI-ului ca cheie primară nu e cosmetică: este același CUI normalizat folosit la deduplicare, așa că aceeași firmă văzută din mai multe surse într-un alt ciclu nu se dublează, ci pur și simplu suprascrie înregistrarea anterioară cu o variantă mai bogată. "Atomic" înseamnă că scrierea fie intră în întregime, fie deloc; dacă procesul moare la jumătatea unui batch, baza rămâne consistentă. Idempotența este completată de reluarea din checkpoint peste JSONL: starea de progres se serializează linie cu linie, iar la repornire daemon-ul citește de unde a rămas în loc să o ia de la capăt. Asta înseamnă că o pană de curent sau un reboot Windows nu costă o zi de muncă, ci câteva minute.
Peste DuckDB stă un jurnal write-ahead în format JSON cu un SINGUR scriitor. Motivul pentru care insist pe "un singur scriitor" este concret: scraping-ul rulează cu mulți workeri în paralel, iar dacă doi ar scrie simultan în același fișier de stare ar apărea curse de scriere: înregistrări parțiale, suprascrieri pierdute, corupere de date. Canalizând toate scrierile printr-un singur jurnal, garantez ordinea și elimin clasa întreagă de bug-uri de concurență, fără să am nevoie de un server de bază de date central.
Cel mai important pas operațional a fost consolidarea: am luat 19 task-uri separate din Task Scheduler și le-am topit într-un SINGUR daemon auto-ritmat. Înainte, fiecare bucată: un refresh ANAF aici, un pull de administratori dincolo, un audit de proxy în altă parte, era un job independent, cu propriul orar, propriul risc de suprapunere și propriul mod de a eșua tăcut. Nouăsprezece ceasuri care nu se vorbeau între ele înseamnă nouăsprezece moduri de a călca unul peste altul. Un daemon unic, auto-ritmat, își planifică singur cadența și știe ce rulează în fiecare moment.
În jurul buclei principale am pus protecțiile care fac diferența între un script și infrastructură. Înainte de fiecare batch verifică spațiul liber pe disc, ca să nu înceapă o scriere pe care nu o poate termina. Rotește un snapshot la fiecare 12 batch-uri, un punct de restaurare regulat, așa că dacă un ciclu strică datele am de unde mă întorc. Reîmprospătează autentificarea la fiecare 6 batch-uri, înainte ca sesiunile să expire, nu după ce deja au picat cereri. Pe 3 eșecuri consecutive intră în backoff de 1h, plafonat la 6h, după care face HALT; logica este să nu suprasolicit o sursă căzută (exact genul de comportament care a dus la banul din 01/05/2026), ci să mă retrag și, dacă nimic nu se reface, să mă opresc curat în loc să ard cote degeaba.
Un lock PID single-instance împiedică două copii ale daemon-ului să pornească simultan, o capcană clasică la repornirile automate. Oprirea este grațioasă: prinde SIGINT, SIGTERM și un fișier STOP, așa că pot opri ciclul curat la jumătate fără să las scrieri pe jumătate sau scrapări orfane. O sondă de sănătate rulează continuu și, dacă trece pe roșu, daemon-ul se auto-oprește în loc să continue să producă date proaste. Separat de daemon, un launch-guard împiedică refresh-urile în bloc să se suprapună, ținând sarcina totală pe ANAF sub 1 cerere/secundă, limita pe care endpoint-ul de bilanț o impune. În fine, izolarea eșecurilor: fiecare sursă e gardată individual, așa că dacă una pică, ECRIS-ul nu răspunde, o cale de proxy e blocată, restul continuă, iar firma căzută merge în PENDING ca să fie reîmbogățită, nu pierdută. Asta este, de fapt, definiția rulării neasistate.
Odată ce o firmă e marcată LEADS, pipeline-ul o scrie direct în CRM-ul FACS: un POST REST către /api/v1/Lead (autentificare Basic), purtând denumirea, CUI-ul, statusul, adresa, website-ul și fiecare contact (nume, rol, email-uri, telefoane). Înainte de scriere rulează două straturi de deduplicare: un filtru local pe CUI și detecția server-side a duplicatelor (409/422), plus un mod dry-run pentru testare. Astfel lead-urile calificate ajung în CRM gata de sunat, fără nicio tastare manuală.
Rezultate și impact în business
Sistemul a îmbogățit 8.307 firme la 99% acoperire și împinge circa 1.000 de lead-uri precalificate pe zi în CRM, cu un throughput de circa 10.000 firme/zi pe un singur cont de proxy. Am trecut de la un proces ținut manual în Excel, unde fiecare firmă era verificată individual prin căutări pe Google și întrebări adresate unui chat AI, cu un decalaj de aproximativ o săptămână între momentul în care o firmă intra în listă și momentul în care era gata de contactat, la un motor care macină mii de firme pe zi, fără supraveghere.
Diferența de fond nu stă în viteza brută, ci în eliminarea atingerii umane din bucla de procesare. În varianta veche, un operator deschidea Excel-ul, copia denumirea firmei, o căuta pe Google, citea rezultatele, punea o întrebare unui chat AI ca să confirme statutul și abia apoi nota un verdict. Fiecare pas era o decizie umană, deci fiecare firmă costa minute și fiecare zi însemna câteva zeci de firme. Noul sistem înlocuiește acea judecată manuală cu surse autoritative consultate programatic: API-ul live ANAF, interogat prin cele trei endpoint-uri ale sale pentru date fiscale, TVA și stare, plus Google Maps și Google Business Profile pentru prezența locală reală și OpenCorporates pentru verificarea structurii juridice. Pentru că aceste surse sunt interogate direct, nu prin lectura unei pagini de către un om, același verdict care lua minute se obține acum în milisecunde, iar acoperirea de 99% spune tocmai asta: aproape nicio firmă nu mai cade din enrichment din lipsa de date.
Throughput-ul de circa 10.000 firme/zi se sprijină pe o singură decizie de inginerie deliberată: un singur cont de proxy BrightData, reglat fin în loc să fie multiplicat. Tentația evidentă ar fi fost să deschizi mai multe conturi pentru a forța mai multe cereri în paralel, dar asta ar fi însemnat costuri mai mari, mai multă complexitate de coordonare și un risc mai mare de blocare. În schimb, contul unic a fost calibrat, cu un ritm al cererilor, rotație și pauze, astfel încât să susțină constant volumul-țintă fără să declanșeze măsuri anti-bot. Este o alegere de tipul 'a face mai mult cu mai puțin': un singur punct de ieșire, predictibil, ușor de monitorizat și de ajustat, în loc de o fermă de conturi greu de ținut sincronizată.
Încadrez impactul de business cu onestitate: pipeline-ul a CONTRIBUIT la aceste rezultate și le-a făcut posibile, nu le-a cauzat software-ul singur. Reactivarea a 28% din clienții inactivi-vechi a devenit posibilă pentru că enrichment-ul a scos la suprafață firme care erau deja în bază, dar care păreau moarte: un operator uman nu avea timp să reverifice mii de conturi vechi, în timp ce motorul le poate reevalua pe toate într-o singură zi și poate semnala pe cele care au redevenit active. Creșterea de +45% a volumului transfrontalier pe trimestru și scurtarea cu 40% a ciclului de vânzare vin din același mecanism: când echipa de vânzări primește circa 1.000 de lead-uri precalificate pe zi, deja filtrate după nevoie și reachabilitate, vorbește cu firmele potrivite mai devreme, fără să piardă timp pe contacte invalide. Retenția de cont de 94% și creșterea de +14% a marjei brute regionale sunt rezultate de business în care pipeline-ul este unul dintre factori, alături de produs, preț și relația comercială, nu cauza unică.
Pentru mine, povestea reală nu sunt cifrele, ci faptul că un proces manual de o săptămână per firmă a devenit infrastructura care rulează singură, ziua și noaptea. Când o sarcină depinde de un om care deschide un fișier și caută manual, ea se oprește când omul pleacă acasă; când devine un motor cu surse autoritative și un proxy reglat fin, ea continuă să livreze lead-uri și la trei dimineața. Aceasta este mutația care contează: nu am făcut o căutare manuală mai rapidă, am eliminat-o complet și am înlocuit-o cu un sistem care nu obosește, nu uită un pas și nu are nevoie de pauză.
Cod sursă: 3 scripturi ANAF/ONRC anonimizate (GitHub)
Cronologie
- 21/04/2026 Primul sweep bulk: 1.629 firme procesate, 1.477 noi în baza de date.
- 23/04/2026 Primele rulări de producție: îmbogățire din surse multiple în paralel, cu poartă de calitate.
- 24/04/2026 Extinderea câmpurilor ANAF (5+ câmpuri/firmă); cifra de afaceri estimată adăugată la scor.
- 30/04/2026 O sursă-cheie devine protejată Cloudflare pe toate căile; serviciul e pus pe pauză.
- 01/05/2026 Ban după 273 de cereri în 5,5 ore → reproiectarea completă a limitării de rată.
- 04/05/2026 Universul de firme descărcat integral local (4 bulk-uri ANAF, 1,4 GB) în DuckDB.
- 13/05/2026 19 job-uri de scheduler consolidate într-un singur daemon auto-temperat.
- 15/05/2026 Ocolirea Cloudflare printr-un browser stealth; sursa repusă în funcțiune.
- 16/05/2026 Throughput de 10.000 firme/zi atins pe un singur cont de proxy.
- 17/05/2026 Jurnal write-ahead JSON (write-then-ingest) + un singur proces de scriere în DuckDB.
- 24/05/2026 Launch-guard care previne rulările de bulk-refresh suprapuse.