A three-person B2B media business at $1.8m of annual recurring revenue is not exotic. What has changed in 2025–2026 is the cost and shape of the machinery behind it. Open-weight models running on hardware you own or rent cheaply now handle the bulk of the reading, sorting and drafting work, while a frontier API is reserved for the handful of steps where reasoning quality matters most. The result is a business whose non-people costs come to roughly 5–8 per cent of revenue, and whose real constraint is editorial judgement, not throughput.
What follows walks through the composite step by step, from team and revenue to stack, costs and the UK and EU legal limits.
Step 1: The team, three roles and what each owns
The composite has three full-time people. Each owns an outcome, not a set of tasks, and each has an explicit list of things the machines are not allowed to decide.
- Editor-publisher. Owns the editorial line, every published sentence and the final sign-off on anything containing a number or a named entity. Also owns sponsor relationships at the senior end, because sponsors buy the editor’s judgement as much as the audience. Spends roughly 60 per cent of the week on reading, deciding and rewriting machine drafts.
- Data and systems engineer. Owns the crawlers, the database, the model serving layer, the verification pipeline and the data products. Treats the newsroom as a production system with on-call duties, logging and error budgets. Writes plain Python; avoids anything they cannot debug at 7am.
- Commercial lead. Owns subscriptions, sponsorship sales, report licensing and the events calendar. Also owns pricing, churn analysis and the email platform.
Freelancers fill gaps: a contract lawyer on retainer for pre-publication review of contentious pieces, an events producer for the roundtables and occasional specialist writers. They sit in the people line of the cost table, not hidden in tooling.
The machines do the reading. People do the deciding. Every role in the team is defined by what it refuses to delegate.
Step 2: The revenue model
The composite earns from four lines. Subscriptions carry half the revenue and most of the predictability; sponsorship is the second leg; data products and events monetise the same underlying corpus a second and third time.
| Line | Unit assumption | Annual revenue | Share |
|---|---|---|---|
| Paid subscriptions | 1,500 subscribers at a blended $600 a year (individual and team seats) | $900,000 | 50% |
| Newsletter sponsorship | 150 slots a year (3 a week, 50 weeks) at $3,600 | $540,000 | 30% |
| Data products and reports | 28 report and dataset licences at $9,000 average | $252,000 | 14% |
| Events | 4 sponsored private roundtables at $27,000 net sponsorship | $108,000 | 6% |
| Total | $1,800,000 | 100% |
Two design choices matter. First, the data products are built from the same structured tables the newsletter draws on (filings, funding events, hiring signals), so their marginal cost is mostly the engineer’s time on packaging. Second, events are deliberately small. Four roundtables a year keep the commercial lead’s calendar sane; a conference business would need a fourth hire.

Step 3: The stack
Model serving: local first, frontier for the hard steps
The composite runs open-weight models on its own hardware for the high-volume, low-stakes work: classification, entity extraction, deduplication checks, first-pass summaries and claim extraction. A sensible default is Ollama or llama.cpp for a single workstation with one or two 24–48GB GPUs, moving to vLLM when concurrent throughput matters (vLLM’s batching earns its complexity once several pipeline workers hit the model at the same time). Quantised mid-sized instruction models handle extraction well; the engineer pins exact model versions and quantisation levels in config, because a silent model swap changes output distributions and breaks downstream thresholds.
A frontier API is called for perhaps 3–5 per cent of model invocations: the final entailment check in verification, difficult synthesis across many documents and the editor’s drafting assistant. This split is the main cost lever. Local inference has a fixed cost and near-zero marginal cost; API calls are variable but buy reasoning quality where an error is expensive.
Vector search: Postgres with pgvector
All documents, chunks, embeddings, claims and verification results live in one Postgres database with the pgvector extension and an HNSW index on the embedding column. At this scale (a few million chunks after two or three years of crawling), a dedicated vector database adds an extra system to back up, secure and keep consistent, for no meaningful gain. The decisive advantages of staying in Postgres are practical:
- Vector similarity and ordinary filters (date range, source type, jurisdiction, licence status) run in one query, with transactional consistency between a document and its embedding.
- One backup and restore story, one access-control model, one place to audit.
- HNSW gives good recall with query latency in the low tens of milliseconds at this size; tuning
ef_searchis usually enough.
The trigger to revisit is tens of millions of vectors with heavy concurrent query load, or a need for features Postgres handles poorly. The composite is nowhere near either.
Embeddings
An open-weight embedding model in the 768–1,024 dimension range, run locally, embeds every chunk. Multilingual capability matters for a transatlantic title that reads German, French and Dutch registry filings. The engineer stores the embedding model name and version alongside each vector, so a future model change can be rolled out as a background re-embedding job rather than a big-bang migration.
Orchestration: plain Python and a job queue
No agent framework. Each pipeline stage is a Python function that reads a job, does one thing and writes its result to Postgres. A simple job queue (a Postgres table with SELECT ... FOR UPDATE SKIP LOCKED, or Redis with a small worker library) handles retries, back-off and concurrency. Every model call logs its prompt hash, model version and output, so any single job can be replayed. Heavy frameworks tend to hide exactly the state you need when debugging a wrong number at publication time.
Step 4: The crawler scripts
The crawlers are the input layer, and they are deliberately conservative. Primary sources come first: RSS and Atom feeds from publishers and companies, and official registries. For UK companies, the Companies House API provides filings, officers and charges under a published rate limit (600 requests per five-minute window at the time of writing). For US issuers, SEC EDGAR full-text search and the EDGAR APIs provide filings, with the SEC’s fair-access policy asking automated users to declare a User-Agent with contact details and stay under ten requests per second. Check both policies before you build; they change.
Rules the composite enforces in code, not in a wiki page:
- Read and obey
robots.txtfor every host, cached and refreshed daily. - Per-host rate limits, with a minimum delay between requests and exponential back-off on 429 and 5xx responses.
- An honest User-Agent naming the publication and a contact address.
- Prefer feeds and APIs over HTML scraping; never bypass paywalls or log-ins.
- Deduplicate by hashing normalised text, so syndicated copies of the same press release are stored once and linked.
import hashlib, time, re
from urllib import robotparser
from urllib.parse import urlparse
import httpx
UA = "VentureSignalBot/1.0 (+mailto:[email protected])"
MIN_DELAY = 2.0 # seconds between requests to the same host
_last_hit, _robots = {}, {}
def allowed(url):
host = urlparse(url).scheme + "://" + urlparse(url).netloc
if host not in _robots:
rp = robotparser.RobotFileParser(host + "/robots.txt")
rp.read()
_robots[host] = rp
return _robots[host].can_fetch(UA, url)
def polite_get(client, url):
if not allowed(url):
return None
host = urlparse(url).netloc
wait = MIN_DELAY - (time.monotonic() - _last_hit.get(host, 0))
if wait > 0:
time.sleep(wait)
resp = client.get(url, headers={"User-Agent": UA}, timeout=20)
_last_hit[host] = time.monotonic()
if resp.status_code == 429 or resp.status_code >= 500:
raise RuntimeError("retry later") # the job queue handles back-off
return resp.text
def content_hash(text):
norm = re.sub(r"\s+", " ", re.sub(r"<[^>]+>", " ", text)).strip().lower()
return hashlib.sha256(norm.encode("utf-8")).hexdigest()
def store_if_new(db, url, text):
h = content_hash(text)
if db.exists("documents", content_hash=h):
db.link_duplicate(url, h) # keep the provenance, skip the work
return False
db.insert("documents", url=url, content_hash=h, body=text)
return True
Exact-hash dedupe catches verbatim copies. Near-duplicates (the same story with a changed headline) are caught later by embedding similarity above a tuned threshold, with the earliest primary source kept as canonical.
Step 5: The automated editorial verification pipeline
This is the part that makes a three-person newsroom credible. Every machine-assisted draft passes through a pipeline whose output is not “true or false” but a list of claims, each with evidence, a confidence score and a status the editor must clear.
| Stage | What it does | Engine | Typical failure it catches |
|---|---|---|---|
| 1. Claim extraction | Splits the draft into atomic, checkable claims (entity, attribute, value, date, source cited) | Local model, structured output | Compound sentences hiding two claims, one of them unsupported |
| 2. Source matching | Retrieves candidate evidence via pgvector plus exact lookups against registry tables | Postgres, pgvector, SQL | Claims with no retrievable source at all |
| 3. Numeric cross-check | Parses numbers, units, currencies and periods; compares with source values deterministically | Plain Python, no model | Millions read as billions, wrong currency, fiscal year confused with calendar year |
| 4. Entailment check | Asks whether the matched evidence actually supports the claim as worded | Frontier API | Overstated wording, wrong attribution, conflated entities |
| 5. Confidence score | Combines source tier, match quality, numeric agreement and entailment into a 0–100 score | Python rules | Weak claims resting on a single secondary source |
| 6. Human sign-off | Editor reviews every claim below 85, every named person and every number, then approves | Editor-publisher | Anything the machines missed, plus judgement on fairness and context |
Where models hallucinate, and how the pipeline catches it
- Numbers and units. Models round, swap scales and invent precision. Stage 3 is deliberately model-free: a parser extracts the figure from the source document and compares it with the draft. Any mismatch blocks the claim.
- Entity conflation. Two companies with similar names, or a parent and a subsidiary, get merged. Registry identifiers (company numbers, CIKs) are attached at extraction, so a claim about the wrong entity fails the exact lookup in stage 2.
- Attribution. Statements drift from “the filing says” to “the chief executive said”. Stage 4 checks wording against evidence, and the house rule is that no quotation is published unless it appears verbatim in a primary source.
- Invented citations. A model can name a report that does not exist. Every citation must resolve to a stored document with a hash; unresolved citations score zero.
- Over-generalisation. “Revenue grew” when one segment grew. Entailment checks catch most of this; the editor catches the rest.
Sign-off is mandatory and logged. No item publishes without a named human approval in the database, and the pipeline cannot set that flag itself. That single constraint does more for accuracy than any threshold.
Step 6: The operating costs
| Cost line | Assumption | Monthly (USD) |
|---|---|---|
| GPU hardware amortisation | Two-GPU workstation, about $16,000–23,000, over 36 months (or equivalent rental) | 450–650 |
| Power and colocation | Rack space or office power for the inference box | 150–300 |
| Frontier API spend | Entailment checks, synthesis and drafting assistant | 800–1,500 |
| Hosting | Managed Postgres, app and worker servers, object storage, backups | 300–600 |
| Data and tools | Paid data feeds, monitoring, analytics, design and productivity software | 800–1,400 |
| Email platform | Paid newsletter platform at roughly 40,000 subscribers (free and paid) | 400–900 |
| Payment processing | Card fees on about $75,000 of monthly subscription billing | 2,000–2,500 |
| Event direct costs | Venues and catering, averaged across the year | 2,500–3,500 |
| Non-people total | 7,400–11,350 | |
| People | Three full-time staff, fully loaded at $180,000–230,000 each a year | 45,000–57,500 |
| Freelancers and retainers | Legal review, events producer, specialist writers | 6,000–10,000 |
| All-in total | 58,400–78,850 |
Against modelled monthly revenue of $150,000, the non-people costs give a gross margin of roughly 92–95 per cent on an infrastructure-and-direct-cost basis. After people and freelancers, contribution is roughly $71,000–92,000 a month, or 47–61 per cent of revenue, before overheads such as accounting, insurance and company administration. Note what dominates: payment fees and events cost more than all the AI infrastructure combined. The GPU line is almost a rounding error; the people line is the business.
AI agents let one person run a media company with no staff and near-100 per cent margins.
In this model, AI and hosting infrastructure costs about 1–2 per cent of revenue, but people still account for roughly 80–90 per cent of total costs, because judgement, sales and sign-off do not automate.
Step 7: What breaks at scale, and what to keep human
Doubling revenue does not double any line in the cost table except payments and people. The breakpoints are elsewhere:
- Editorial review becomes the bottleneck. Throughput is capped by how many low-confidence claims the editor can clear. The first scale hire is a second editor, not more compute.
- Source drift. Feeds change format, registries change APIs and sites add bot protection. Silent crawler failures produce a newsletter that looks fine but is missing a sector. Monitor volume per source and alert on drops.
- Model upgrades shift thresholds. A better model can still change score distributions. Keep a labelled test set of a few hundred claims and re-run it before any model change.
- Sponsor conflicts. As sponsors multiply, the chance of covering a sponsor critically rises. This is a policy problem; write the rule down before it happens.
Keep human: final sign-off on every published claim, any statement about a named person, story selection, sponsor relationships, pricing, corrections and anything that a lawyer might later read.
Step 8: The UK and EU angle
A US-built crawling playbook does not transfer cleanly. Three points matter for a commercial publication operating in the UK or EU. This is general information on rules that are under active review; check the current position before relying on it.
- UK copyright. The UK text and data mining exception in section 29A of the Copyright, Designs and Patents Act 1988 covers computational analysis for non-commercial research only. A subscription business is commercial, so it cannot rely on it to copy third-party content for analysis. The practical response is to prioritise open registries and licensed feeds, store links and short extracts rather than full copies where possible, and seek licences for anything else. The UK government has consulted on changes to copyright and AI; at the time of writing, check the current guidance.
- EU copyright. Article 4 of the Digital Single Market Directive (EU) 2019/790 permits text and data mining, including for commercial purposes, of lawfully accessible works unless the rights-holder has expressly reserved those rights, for online content in a machine-readable way. Crawlers should therefore detect and honour opt-outs (robots.txt and other machine-readable reservations, plus terms of service) and log that they did so. National implementations vary.
- UK GDPR. Crawled content contains personal data: directors’ names in registry filings, executives quoted in press releases, authors’ bylines. Processing it needs a lawful basis (often legitimate interests, supported by a documented assessment), data minimisation, retention limits and transparency. The Data Protection Act 2018 contains an exemption for journalism, but it is conditional and does not remove all obligations. The ICO’s guidance is the place to start; EU GDPR applies in parallel for EU data subjects.
The engineering consequence is simple: tag every stored document with its source type, jurisdiction and licence basis at ingest, so that a retention or takedown request becomes a query, not a project.
This section is general information, not legal advice. Take specialist advice for your own circumstances.



