all builds >> N. GUGLIELMI
>> nicola@guglielmi ~ / labs / faro

Faro

Commercial intelligence for IT staffing firms, built backwards. Everyone else sells tender coverage: here is every notice, go read them. Faro starts from the people sitting on the bench this week and asks which demand they could actually cover, then hands each salesperson a call list with the reason already written. The scoring is deterministic on purpose, and the model is kept well away from it.

Python 3.12 FastAPI Cloud Run + Jobs Cloud SQL / Postgres TED · ANAC open data Gemini (bounded) Multi-tenant
>> THE_PROBLEM

A staffing firm's problem is not finding tenders. Incumbent aggregators already publish more notices than anyone can read, and they win on coverage. The problem is the sentence after that: can we cover this one, with whom, at what margin. That question needs the bench on one side of the equation, and the bench is exactly what an aggregator has never seen.

So Faro reverses the arrow. It reads the firm's own skill matrix, then goes looking for demand that those specific people could serve: European tenders, integrators who just won something and now have to staff it, subcontracting channels that are already open, and job ads nobody has managed to fill. What comes out is not a feed. It is a short list per salesperson, cut to the number of calls a week actually holds.

>> BENCH_MEETS_DEMAND
supply, the bench Skill matrixExcel, read by headernot by filename Auditdeclared vs verifiableruns before any score Taxonomy323 raw valuesto canonical slugs demand, the market TED · ANAC · adsEU tenders, awards,subcontracts, postings RelevanceIT / maybe / outstaffing CPV is any trade CPV to familytitles rarely namea technology Matchingcoverage x winnabilitygeometric meanblockers zero it out Call listbucketed to the callsa week actually holds The scraper is a separate process on a schedule. The web service never queries a source, so a dead feed degrades to yesterday's data with a warning, not a blank page.

> one image, two commands: the same container runs the web service and the scheduled scrape job, so they cannot drift apart.

>> DECISIONS_THAT_MATTERED
[ 01 ]

The model maps columns. That is the whole job.

The same spreadsheet, handed to the model three times at temperature zero, came back with three different column mappings. A number that decides who gets called cannot be built on that. So a deterministic alias table runs first and maps ten columns out of ten on a realistic HR export with no API call at all, and the model is only ever asked which column is which, never what a person is worth. The cost is an alias table that will never be finished, and unknown layouts get refused rather than guessed.

[ 02 ]

Audit the data before you are allowed to score it

The first real export was worse than anyone would admit out loud: 294 people collapsing into 132 identifiers, one bucket holding thirteen different Andreas, and availability and cost empty on all 4,400 rows. Pseudonymisation leaked too, 116 values out of 140 relinked to the full name by plain substring match against the other file. So the audit runs first and the scoring stays off while it is red. A matching engine on top of that data would have produced confident nonsense.

[ 03 ]

A job ad gets better as it gets older

Everywhere else in the product fresh data wins. Here it inverts, and that is precisely why it is written down. A posting published yesterday is a bad lead: it means headcount budget, and you are competing with their own recruiters, who over twelve months cost less than your day rate. An ad still open after ninety days is documented proof that recruiting failed, which is the moment staffing gets bought.

[ 04 ]

Two scores, never multiplied into one

Skill level and how recently it was verified were briefly folded into a single number. Against a 2022 extraction everything sank below threshold and the product's answer became "zero people available", which is technically defensible and commercially useless. They went back to being two columns: what the firm declares, and what it can actually stand behind. Oracle read 73 declared against 46 verifiable, and that gap is the re-verification backlog, not a rounding error.

[ 05 ]

Every integration trap, written down as it was found

Public data sources punish assumptions. Omitting one pagination flag on the EU tender API silently drops a hundred notices out of 191, with no error to notice. Consortium bids arrive as a single unsplit string, so one company across four groupings reads as four, and the strongest signal in the dataset, who keeps winning, quietly disappears. Contact emails are taken only from sole-winner notices, because on a consortium you get the lead partner's address, and no contact beats a wrong one.

[ 06 ]

The moat argument that stopped being true

The original case for building this was that a firm's bench knowledge cannot be copied, so an internal tool is defensible. Selling the same engine to several firms cancels that argument outright: the bench belongs to each client, the engine is generic. Rather than quietly keeping the old slide, the decision log records that the defensibility is now amortisation, one scrape serving every tenant, plus conversion data across them. Noticing your own reasoning has expired is cheaper than being told later.

>> WHY_THIS_MATTERS_TO_YOU

Faro is what I do when a domain looks like it needs AI and mostly needs arithmetic you can defend. The model earns its place on exactly one task, the scoring stays reproducible and contestable row by row, and the ugly parts, messy source data and hostile public APIs, get audited rather than averaged. If you have a decision buried in data nobody trusts yet, that is the same job. Let's talk.