Request a callbackBook a call
← All posts

Build an AI Sales Research Agent: Architecture, Freshness Strategy and Cost Per Brief (2026)

TL;DR
  • A pre-call account brief costs about 22.6 cents cold and 9.8 cents warm on the arithmetic below, against roughly $11 of account executive time it displaces. The economics are so lopsided that cost is not the interesting engineering problem here. Freshness is.
  • The wedge in a research agent is the cache TTL table, not the prompt. Firmographics stay true for a month; a funding round is wrong within hours. A single global TTL either triples your cost or ships stale facts into a sales call, and both failures are invisible until a rep is embarrassed live.
  • Every claim in the brief must carry a source URL, a retrieval timestamp and a quoted span that literally appears in the source. A brief without those three is a plausible-sounding document that a rep will read aloud to a prospect.
One brief, six stages
  1. 1
    1 · Resolve the account$0.00-$0.08

    Email domain to CRM record to a canonical company entity, with a confidence score. If resolution is ambiguous, the run stops here and asks. It never guesses between two similarly named companies.

  2. 2
    2 · Plan the research$0.016

    One planner turn produces a typed research plan: which sources to hit, which questions each must answer, and what the caller already knows so the agent does not re-derive it.

  3. 3
    3 · Fan out, in parallel$0.042 search + $0.029 extract

    Six to ten source calls run concurrently behind a freshness cache. Cached sources cost nothing and return in milliseconds; cold sources cost money and take seconds.

  4. 4
    4 · Reconcile conflictsin synthesis

    Two sources will disagree about headcount, funding and job titles. The agent must record both with dates and prefer the most recent primary source, not silently pick one.

  5. 5
    5 · Synthesise the brief$0.0446

    A structured document, not prose, with every claim carrying a source URL, a retrieval timestamp and a quoted span. Talking points are explicitly labelled as inference.

  6. 6
    6 · Verify and deliver$0.014

    A cheap model plus deterministic span matching checks every claim against its source before the brief reaches a human. Unverifiable claims are dropped, not softened.

Stage six is the one that separates a research agent from a plausible-text generator, and stage three is the one that separates a 22.6-cent brief from a 9.8-cent one. Neither is a prompting problem: one is a validation problem and the other is a cache-design problem.

What is an AI sales research agent, architecturally?

A planner, a parallel fan-out over six to ten sources behind a per-source freshness cache, a conflict-reconciliation step, and a citation-checked brief generator. It is a retrieval and verification system with a little agency at the front, not a conversational agent. Building it as a chat loop is the most common way teams make it slow, expensive and wrong.

The product is narrow and that is its strength. Thirty minutes before a discovery call, a rep receives a one-page brief: what the company does, what changed in the last ninety days, who is on the call and what they own, what our CRM already knows about this account, what the product telemetry says if they are an existing customer, and three talking points explicitly marked as inference rather than fact. That is the whole surface area.

This is a reference design: how I would build it and what the arithmetic says it costs. It draws on architecting AccioMatrix, an event-driven platform orchestrating multiple LLMs, agents and MCP tooling for 20+ enterprise clients, but I have not shipped a sales research agent in production, and nothing below is an invoice. It is a model with its inputs printed.

The architectural shape differs from a support agent in one decisive way. A support agent is sequential because each tool result changes what it should do next. A research agent is overwhelmingly parallel: it knows on turn one which sources it wants, and it should hit all of them at once. Getting that right takes a nine-source brief from roughly 45 seconds to roughly 9, and the cost is identical. Anyone building this as a sequential agent loop is paying a latency tax for no benefit.

The second difference: freshness is a first-class architectural concern, not a caching optimisation. In support, a document is either current or it is not. In sales research, every fact has its own half-life. A company's headquarters is stable for years, its headcount for a quarter, its funding status for hours after an announcement. A single TTL cannot serve them all, and the table that assigns a TTL per source matters more to output quality than any model choice you will make.

ComponentWhat it doesImplementationCost per briefPrimary failure modeSkip in v1?
Account resolverEmail domain to CRM record to canonical company entity, with a confidence score and a refusal pathDeterministic domain matching first, enrichment API second, model never$0.00 cached / $0.08 coldResolves to the wrong company with a similar name; every downstream fact is confidently about someone elseNo — this is the load-bearing step
Research plannerOne typed plan: sources to hit, question each must answer, what the caller already knowsSonnet-class, structured output, ~6k in / 400 out$0.016Plans nine sources for an account where four would do, tripling cost for no added signalYes — a fixed plan works in v1
Fan-out workersSix to ten source calls concurrently, each behind the freshness cachePromise.all over typed source adapters with per-source timeouts$0.042 search + $0.029 extractOne slow source holds the whole brief; no per-source timeout means the p99 is the worst sourceNo
Freshness cachePer-source TTL, keyed by entity and source, with the retrieval timestamp stored alongside the valueRedis for hot reads, Postgres as the durable record with timestampsNegative — removes 57%One global TTL: either triple cost or stale funding news in a live callNo — this is the wedge
MCP tool surfaceCRM read, enrichment, web search, page fetch, product telemetry, billing stateMCP servers behind a gateway, ~9 tools, ~1.4k tokens of schema per turnIn the turn costsAn enrichment tool returns 40k tokens of raw JSON and blows the synthesis contextGateway: no
Conflict reconciliationRecords disagreements with dates and sources instead of silently picking oneDeterministic rules by source precedence, model only for genuine ambiguityIn synthesisSilently picks the wrong headcount; the rep quotes a number that is two years oldNo
Brief synthesisTyped document with a claim list, each carrying URL, retrieval timestamp and quoted spanSonnet-class, ~18k in / 1.4k out, schema-validated output$0.0446Fluent prose with no verifiable claims — the failure that looks like successNo
Claim verificationEvery claim's quoted span must literally appear in the fetched source textDeterministic substring match plus a cheap-model verifier for paraphrase$0.014Verifier passes a paraphrase that reverses the meaning; drop, do not softenNo
DeliverySlack DM, calendar attachment or a CRM note, with the trace id visibleTemplated, plus a one-click was-this-useful signal~$0Arrives after the call started; useless at any quality levelNo

What does the full architecture look like?

Twelve components in four columns: triggers and resolution, planning and parallel fan-out, the MCP tool surface, and reconciliation, synthesis and delivery. The freshness cache sits underneath the fan-out rather than beside it, because every source call goes through it and roughly 57% of them never leave the building.

Start at the trigger, because it determines your latency budget and therefore your whole design. A calendar-driven brief fires thirty minutes before a meeting and can take three minutes to build, so you can be thorough, use slower sources and retry generously. A rep clicking research this account in the CRM is waiting, and the budget is under twelve seconds. Same components, different systems. Build the scheduled one first: it is more useful, cheaper per brief because it can batch, and far easier to get right.

Account resolution carries the most risk per line of code. Domain-to-CRM matching is deterministic and correct. Falling back to a model to disambiguate two similarly named companies is where briefs go catastrophically wrong, because every subsequent fact is confidently about the wrong organisation and nothing downstream catches it. On ambiguity, stop and ask. The engineering is a confidence score with a refusal threshold. A research agent that refuses one brief in fifty is a good research agent.

The fan-out layer is where the parallelism lives and where per-source timeouts are non-negotiable. Nine sources called concurrently with a four-second timeout each produce a brief in about nine seconds with occasional gaps. Nine with no timeout produce a brief whose latency equals the slowest external service on the internet that day. Mark missing sources explicitly. A brief that says no recent news found, checked at 09:14 is honest and useful; one that quietly omits the section teaches the rep nothing about what was not checked.

The MCP surface splits into three groups with very different trust properties. CRM and product telemetry are yours: authoritative, cheap, safe to trust. Enrichment APIs are commercial data of variable accuracy. Clearbit's standalone product no longer exists as such, folded into HubSpot as Breeze Intelligence, itself a lesson about depending on a single enrichment vendor. Web content is untrusted input that reaches your model, so prompt-injection defence at ingestion is a requirement, not a nicety. The gateway pattern that handles all three is in MCP in production.

The system
Sales research agent: full reference architectureconfidence >= 0.9ambiguous: ask, do not guesstyped plan57% served hereevery callsanitised
Triggerscalendar T-30 · CRM stage change · manual
Account resolverdomain to CRM id to entity + confidence
Freshness cacheper-source TTL · value + retrieved_at
Research plannertyped plan · which sources, which questions
Parallel fan-out9 sources · 4s timeout each · partial ok
Run + step journalevery source call, cost, latency, cache status
MCP: internalCRM · telemetry · billing · past notes
MCP: enrichmentfirmographics · contacts · tech stack
MCP: websearch + fetch · untrusted input
Conflict reconciliationrecord disagreements with dates + source
Brief synthesis + verifytyped claims · span check · drop unverified
DeliverySlack · calendar · CRM note · trace id
The dashed edge from the resolver straight to delivery is the most important path in the diagram and the one nobody builds. A brief that says I found two companies matching acme.io and could not tell which one you are meeting is worth more than a confident brief about the wrong one, and it costs a fraction of a cent instead of 22.6.

What happens when one brief is generated, end to end?

Trigger, resolve, plan, nine parallel source calls of which five hit cache, extract and summarise the fresh pages, reconcile conflicts, synthesise a typed brief, verify every claim against its source, deliver. About nine seconds wall clock on the parallel path, four model calls, 22.6 cents cold.

The step to watch is extraction. Twelve fetched pages at roughly 2,000 tokens each is 24,000 tokens, and dumping all of it into the synthesis context is the naive design that makes this product expensive and worse. Map-reduce instead: summarise each page independently on a cheap model against the specific question the plan assigned it, then synthesise from the summaries. That turns 24,000 tokens of raw page content into about 3,000 tokens of targeted summary for roughly 2.9 cents, and it improves the output because synthesis is no longer reading boilerplate navigation menus.

The synthesis call is the only expensive model call in the pipeline, at 4.5 cents of a 22.6-cent brief. Spend model quality here and nowhere else. Extraction, verification and reconciliation all run acceptably on a Haiku- or Flash-class model. The step that has to hold nine sources in mind, notice that two disagree and write something a human will act on is the one that justifies a Sonnet-class model. That asymmetry is the whole routing policy, and the general machinery is in LLM routing and caching, cost per request.

Verification runs after synthesis and is allowed to delete. A claim whose quoted span cannot be found in the fetched source text is removed, and the brief notes that a claim was dropped. It is tempting to soften instead, to rewrite the claim as reportedly or according to some sources, and that is precisely wrong, because a hedged sentence in a sales brief still ends up in the rep's mouth on the call. Drop it.

The brief path
One account brief, end to end, with cost on every callSchedulerResolverPlannerCacheFan-outExtractSynthVerify
meeting at 10:00 with 3 attendees, T-30 trigger
domain acme.io to CRM account, confidence 0.97
deterministic, $0
entity + what CRM already knows
plan: 6k in / 400 out
$0.016 · Sonnet 5
9 source calls, 4s timeout each
lookup by (entity, source, ttl)
5 hits: firmographics, tech stack, CRM, telemetry, past notes
4 misses: news, funding, jobs, exec moves
6 searches at $7/1k = $0.042
12 fetched pages, ~24k tokens
map-reduce: 24k in / 3k out
$0.029 · Gemini 3.7 Flash
3k of targeted summaries + 5 cached source values
synthesise: 18k in (3k cached) / 1.4k out
$0.0446 · Sonnet 5
24 typed claims with spans and URLs
span match + verifier: 12k in / 400 out
$0.014 · Haiku 4.5
22 claims verified, 2 dropped, brief delivered
Total modelled cost: $0.2261 cold. Five of nine sources came from cache at zero marginal cost, which is why the same brief for an account researched last week costs 9.8 cents. Two claims were dropped by verification, a healthy rate. A system where verification never drops anything is a system whose verifier is not working.
Where nine seconds goes on the parallel path
12210ms totalbudget 12000ms
Resolve account (deterministic)210ms
Plan the research1400ms
Fan-out: slowest source (4s cap)3900ms
Extract 12 pages (map, parallel)1600ms
Synthesise brief4200ms
Verify claims900ms
Roughly 12.2 seconds, and the fan-out contributes 3.9 of them because it is capped, not because it is slow. Run those nine sources sequentially and this becomes about 45 seconds for identical cost and identical output. Parallelism is the single highest-return design decision in this product, and it is a structural choice you cannot retrofit onto a conversational agent loop.

How do you keep the brief fresh without paying for every fact every time?

With a TTL per source rather than a TTL per cache. Firmographics are true for thirty days. A tech-stack signal is true for fourteen. Job postings shift weekly. Funding announcements and executive changes are wrong within hours of breaking. One table assigning a half-life to each source is the difference between a 9.8-cent brief and a 22.6-cent one, and between a fresh brief and an embarrassing one.

The mechanism is simple and the discipline is not. Cache the value with the timestamp it was retrieved at and the source it came from, then let each source declare its own TTL. On a cache hit, the brief still prints the retrieval date next to the fact (as of 12 August, headcount was approximately 340), which converts a staleness risk into an honest statement. A rep who can see a number is twelve days old handles it correctly. A rep reading an undated number will not.

Two refinements earn their keep quickly. First, event-driven invalidation: if a news search returns a funding or acquisition story for an entity, invalidate that entity's firmographic cache immediately rather than waiting thirty days, because headcount and valuation just changed. Second, a warm-ahead job: for accounts with a meeting in the next seven days, refresh the slow, expensive sources overnight in batch, so the T-30 brief is nearly all cache hits. Batch inference bills at roughly half interactive rates on the major providers, which makes warm-ahead cheaper per fact as well as faster.

The chart below is the argument for doing this properly. Sweeping a single global TTL from one hour to ninety days moves cost per brief from about 22.6 cents to about 9.5 cents while the modelled stale-fact rate rises from well under 1% to roughly 26%. No setting on that curve is right for all sources, which is exactly why the per-source table exists. You want one-hour behaviour for funding news and thirty-day behaviour for headquarters, and a global setting forces you to pick one and be wrong about the other.

One caveat about the stale-fact curve: those percentages are a model, derived from plausible change rates per source class, not a measurement. The shape is robust (fast-moving sources decay quickly, slow ones do not) but the exact numbers depend on your account mix. An agent researching Series A startups faces a much steeper curve than one researching listed enterprises. Measure your own before quoting anyone else's.

freshness/source-policy.ts
export type SourceId =
  | "crm.account"
  | "crm.notes"
  | "product.telemetry"
  | "billing.state"
  | "enrich.firmographics"
  | "enrich.techstack"
  | "web.news"
  | "web.funding"
  | "web.jobs"
  | "web.exec_changes";

type SourcePolicy = {
  ttlSeconds: number;
  /** higher wins when two sources disagree about the same field */
  precedence: number;
  /** roughly what one cold call costs, in USD */
  coldCostUsd: number;
  /** other caches to blow away when this source returns a hit */
  invalidatesOnEvent?: SourceId[];
};

const HOUR = 3600;
const DAY = 24 * HOUR;

export const SOURCE_POLICY: Record<SourceId, SourcePolicy> = {
  "crm.account":          { ttlSeconds: 5 * 60,  precedence: 100, coldCostUsd: 0.0000 },
  "crm.notes":            { ttlSeconds: 5 * 60,  precedence: 100, coldCostUsd: 0.0000 },
  "product.telemetry":    { ttlSeconds: 1 * HOUR, precedence: 100, coldCostUsd: 0.0000 },
  "billing.state":        { ttlSeconds: 15 * 60, precedence: 100, coldCostUsd: 0.0000 },
  "enrich.firmographics": { ttlSeconds: 30 * DAY, precedence: 60,  coldCostUsd: 0.0800 },
  "enrich.techstack":     { ttlSeconds: 14 * DAY, precedence: 50,  coldCostUsd: 0.0000 },
  "web.news":             { ttlSeconds: 6 * HOUR, precedence: 70,  coldCostUsd: 0.0140 },
  "web.funding":          { ttlSeconds: 2 * HOUR, precedence: 80,  coldCostUsd: 0.0140,
                            invalidatesOnEvent: ["enrich.firmographics"] },
  "web.jobs":             { ttlSeconds: 7 * DAY,  precedence: 40,  coldCostUsd: 0.0070 },
  "web.exec_changes":     { ttlSeconds: 12 * HOUR, precedence: 75, coldCostUsd: 0.0140,
                            invalidatesOnEvent: ["crm.notes"] },
};

export type Cached<T> = { value: T; retrievedAt: string; source: SourceId };

export function isFresh(entry: Cached<unknown>, now = Date.now()): boolean {
  const age = (now - Date.parse(entry.retrievedAt)) / 1000;
  return age < SOURCE_POLICY[entry.source].ttlSeconds;
}

/** Facts are always rendered with their age. Never print an undated number. */
export function renderFact(label: string, value: string, e: Cached<unknown>): string {
  const days = Math.floor((Date.now() - Date.parse(e.retrievedAt)) / 86400000);
  const age = days === 0 ? "today" : days === 1 ? "yesterday" : days + " days ago";
  return label + ": " + value + " (source: " + e.source + ", checked " + age + ")";
}
The freshness table as code. Three details matter. Every cached value stores retrievedAt so the brief can print a fact's age rather than hide it. Each source declares its own TTL and trust precedence, which the conflict reconciler uses when two sources disagree. And invalidatesOnEvent is the event-driven refresh: a funding story invalidates firmographics immediately, because the numbers just changed.
Cost per brief vs stale-fact rate, sweeping a single global TTL
292215701 hour6 hours24 hours7 days30 days90 daysCost (cents per brief) / stale facts (%)Global cache TTL
No single TTL is right, hence the per-source table
Cost per brief (cents)Modelled stale-fact rate (%)
The two curves cross somewhere between 24 hours and 7 days, which is why teams that pick one number tend to land there, and why they then ship stale funding news to a rep about once a fortnight while still overpaying to re-fetch a company's headquarters. The stale-fact percentages are modelled from plausible per-source change rates, not measured. The shape is reliable; the exact values depend on your account mix.
Nine sources, with TTL, cost and the honest caveat on each
 TTLCold costTrustWhat goes wrong
CRM account + open opportunities5 min$0AuthoritativeSales-entered data is stale and optimistic; the agent inherits both
CRM notes from previous calls5 min$0AuthoritativeNotes are unstructured and often wrong; treat as claims, not facts
Product usage telemetry1 hour$0AuthoritativeOnly exists for current customers — the highest-value source you cannot use on new logos
Billing and contract state15 min$0AuthoritativeExposing renewal amounts to the wrong rep is a permissions problem, not a data problem
Firmographics (enrichment API)30 days$0.08Commercial, variableHeadcount figures lag reality by a quarter; vendor consolidation is a real dependency risk
Tech stack signals14 days$0WeakDetects a script tag, not a purchasing decision. Useful as a hypothesis, never as a fact
News and press6 hours$0.014MediumPress releases are marketing; the agent will repeat a claim the company made about itself
Funding and acquisitions2 hours$0.014Medium-highThe fastest-decaying fact in the brief and the most embarrassing one to get wrong
Job postings7 days$0.007MediumGreat buying signal, frequently misread. Ten open SRE roles is a hypothesis about priorities
The four internal sources are free, authoritative and refreshed in minutes, and they are the ones teams instrument last because building a CRM MCP tool is less exciting than adding a web search. Reverse that order. A brief built purely on public web data is a brief your competitor could also generate; a brief that knows what your own product telemetry says is one only you can write.

How do you stop the brief from making things up?

By making the brief a typed data structure rather than prose, requiring every claim to carry a source URL, a retrieval timestamp and a quoted span, and deleting any claim whose span cannot be found in the fetched source. Inference is allowed but must be labelled as inference and must reference the claims it rests on.

The structural decision does the work. If the output is a paragraph, there is nothing to verify; you would be grading prose with prose. If the output is a list of claims, each with a field pointing at the exact sentence in the exact document that supports it, verification becomes a substring match: deterministic, free and impossible to argue with. Same discipline as citation-span validation in a support answer, applied to a document with twenty-four claims instead of three.

Separating fact from inference is the second half. A brief is genuinely more useful when it says they posted eight SRE roles in the last month and are likely investing in reliability, but those are two different kinds of sentence. The first is a claim with a URL. The second is an inference with no source, and it must render differently so a rep never reads an inference aloud as a fact. Enforce it in the schema, not the prompt: inferences live in a different array with a different required shape and a mandatory list of the claim ids they depend on.

The third mechanism is a refusal path with teeth. If fewer than a threshold number of claims survive verification, do not deliver a brief. Deliver a note: the agent could not find enough verifiable information about this account, here is what it checked, and here are the two questions worth asking on the call. Reps trust that behaviour and it is cheap to build. What destroys trust permanently is a brief that pads a thin account with generic industry commentary, because the rep discovers the padding on the call.

None of this requires a smarter model. It requires an output schema, a substring matcher and the discipline to delete. That is roughly 300 lines of code, and it is the difference between a research agent and an expensive text generator.

brief/schema.ts
import { z } from "zod";

export const ClaimSchema = z.object({
  id: z.string(),
  text: z.string().min(1),
  source_url: z.string().url(),
  source_id: z.string(),
  retrieved_at: z.string().datetime(),
  quoted_span: z.string().min(20),
  category: z.enum([
    "company", "funding", "people", "hiring",
    "tech", "usage", "relationship", "news",
  ]),
});

export const InferenceSchema = z.object({
  text: z.string().min(1),
  confidence: z.enum(["low", "medium", "high"]),
  /** an inference with no supporting claims is not shippable */
  based_on: z.array(z.string()).min(1),
});

export const BriefSchema = z.object({
  account_id: z.string(),
  generated_at: z.string().datetime(),
  claims: z.array(ClaimSchema),
  inferences: z.array(InferenceSchema),
  open_questions: z.array(z.string()).max(5),
  sources_checked: z.array(z.object({
    source_id: z.string(),
    status: z.enum(["hit", "miss", "timeout", "error", "cached"]),
    checked_at: z.string().datetime(),
  })),
});

export type Brief = z.infer<typeof BriefSchema>;

const MIN_VERIFIED_CLAIMS = 6;

export function verifyBrief(
  brief: Brief,
  sourceText: Map<string, string>,
): { brief: Brief; dropped: string[]; deliverable: boolean } {
  const dropped: string[] = [];

  const claims = brief.claims.filter((c) => {
    const body = sourceText.get(c.source_id);
    if (!body) { dropped.push(c.id); return false; }
    const ok = norm(body).includes(norm(c.quoted_span));
    if (!ok) dropped.push(c.id);
    return ok;
  });

  const keptIds = new Set(claims.map((c) => c.id));
  // an inference loses its support when the claim under it is dropped
  const inferences = brief.inferences.filter(
    (i) => i.based_on.every((id) => keptIds.has(id)),
  );

  return {
    brief: { ...brief, claims, inferences },
    dropped,
    deliverable: claims.length >= MIN_VERIFIED_CLAIMS,
  };
}

function norm(s: string): string {
  return s.toLowerCase().replace(/[^a-z0-9 ]+/g, " ").replace(/\s+/g, " ").trim();
}
The output contract. Claims and inferences are separate arrays with different required fields, which makes the fact-versus-guess distinction a type error rather than a style guideline. verifyBrief is deterministic and free: it either finds the quoted span in the source text or it drops the claim. The refusal threshold at the bottom is what stops a thin account producing a padded brief.
Checklist
The seven rules that keep a research brief honest
  • Every claim carries a URL, a source id, a retrieval timestamp and a quoted spanTurns verification from a judgement call into a substring match that costs nothing.
  • Facts and inferences live in different arrays and render differentlyA rep must never read an inference aloud as a fact. Enforce in the schema, not the prompt.
  • Dropped claims are deleted, never softened into hedged proseReportedly and according to some sources still end up in the rep's mouth on the call.
  • Every fact prints its ageAs of 12 August, headcount was approximately 340. An undated number invites a rep to state it as current.
  • Sources checked and not found are listed explicitlyNo recent news found, checked 09:14 is information. A silently omitted section is not.
  • Below six verified claims, deliver a note rather than a briefPadding a thin account with industry generalities is the fastest way to lose a sales team's trust.
  • The synthesis step has no toolsRead and write only. Makes a successful prompt injection produce a bad brief rather than a bad API call.
Six of these are about a hundred lines each. The last one is a design constraint that costs nothing at build time and is nearly impossible to retrofit once a synthesis agent has grown tool access, which is why it belongs in the first architecture review.

What does one brief cost?

About 22.6 cents cold and 9.8 cents warm, against roughly $11 of account executive time it displaces. At 500 briefs a month with 55% warm, the running cost is under $80. This is the rare AI product where the unit economics are not the interesting problem, which is precisely why the engineering attention belongs on freshness and verification instead.

The composition is worth internalising because it is unlike the support agent. There, 90% of the cost was agent turns. Here, the single synthesis call is 20% of the bill, the enrichment API call is 35%, search and extraction together are 31%, and everything else is noise. The dominant line is a third-party API, not a model, so the cost lever is the cache TTL table, not model selection. Halving the enrichment call rate with a thirty-day firmographic TTL saves more than switching the synthesis model to a cheaper tier, and it does not degrade the output.

The comparison that actually gets this funded is the human one. A rep doing thorough pre-call research on an unfamiliar account spends fifteen to thirty minutes across a CRM, a website, LinkedIn and a news search. Model that at 25 minutes and $45/hour fully loaded and it is $18.75 of time, of which a good brief plausibly displaces 15 minutes, or about $11.25. At 500 briefs a month that is roughly $5,600 of recovered selling time against under $80 of running cost.

Be careful with that number in front of a CFO, because recovered time is not recovered money unless the rep does something else with it. The brief converts unstructured preparation into structured preparation, and its real effect shows up as more calls prepared for at all, not as headcount reduction. Reps skip preparation under pipeline pressure; a brief that arrives automatically is prepared-for by default. That is the actual value, and it is harder to put on a slide.

Build cost is roughly six engineer-weeks for a two-person team, which at typical blended rates is the dominant number in the first year. The running cost is a rounding error against it. The break-even question is not build-versus-buy but build-versus-nothing, and it turns on whether your reps would actually read the brief. Ship it into Slack thirty minutes before the meeting, measure the open rate for two weeks, and let that decide.

Line itemModel / vendor / rateUnitsCold briefWarm briefNote
Account resolutionDeterministic domain to CRM match1 lookup$0.0000$0.0000Model is never used to disambiguate; ambiguity stops the run
Firmographic enrichmentEnrichment API, modelled at $0.08/record1 record$0.0800$0.000035% of a cold brief. 30-day TTL is the single biggest cost lever
Research planningSonnet 5 · $2 / $10 per 1M6,000 in / 400 out$0.0160$0.0160A fixed plan removes this entirely in v1
Web searchExa modelled at $7 per 1,000 requests6 queries cold / 2 warm$0.0420$0.0140Brave at $5/1k and Tavily at ~$7.50/1k are the alternatives
Page extraction (map)Gemini 3.7 Flash · $0.75 / $3.75 per 1M24,000 in / 3,000 out$0.0293$0.0098Map-reduce, not one giant context. Improves quality and cost together
Brief synthesisSonnet 518,000 in (3,000 cached) / 1,400 out$0.0446$0.0446The only call that justifies a frontier-adjacent model
Claim verificationHaiku 4.5 · $1 / $5 per 1M12,000 in / 400 out$0.0140$0.0140Span matching is free; only paraphrase adjudication is a model call
Cache + journal storagePostgres + Redis, amortised~40 rows$0.0002$0.0002Store retrieved_at with every value or the whole scheme is decorative
Total per brief$0.2261$0.0986Modelled from Aug 2026 list prices, not measured
Human equivalentAE at $45/hr fully loaded15 min displaced of 25$11.25$11.25Recovered time, not recovered headcount. Be honest about which
Where 22.6 cents goes on a cold brief
$0.226cold brief
  • Enrichment API record$0.080 · 35%
  • Brief synthesis (Sonnet 5)$0.045 · 20%
  • Web search (6 queries)$0.042 · 19%
  • Page extraction (Flash, map)$0.029 · 13%
  • Planning (Sonnet 5)$0.016 · 7%
  • Verification (Haiku 4.5)$0.014 · 6%
The largest slice is not a model. In a research agent the dominant cost is third-party data, which flips the usual optimisation order: cache policy beats model selection, and the second-largest lever is reducing search queries through a better plan rather than reducing tokens through a cheaper tier.
The four numbers for the business case
$0.226
modelled cost of a cold brief on a new account, all sources fetched
$0.099
modelled cost of a warm brief where five of nine sources are within TTL
-56%
~$11.25
AE time displaced per brief at $45/hr, 15 of 25 research minutes
6
engineer-weeks to a production v1 for a two-person team
The gap between the second and third figures is roughly 100x, which is why nobody should spend a week optimising the model spend on this product. Spend it on the resolver, the TTL table and the verifier. Those three determine whether the brief is trusted, and an untrusted brief has a return of exactly zero regardless of what it cost.

What breaks in production, and how would you know?

Nine things, and the worst is the quietest. An agent that resolves acme.io to the wrong Acme produces a fluent, well-cited, entirely irrelevant brief, and every downstream check passes because the claims genuinely are supported by their sources. They are just sources about a different company. No error rate moves. A rep finds out live.

That failure has a specific fix and it is not a better model. It is a confidence score on resolution with a refusal threshold, plus one deterministic cross-check: the resolved entity's domain must appear in the CRM record, or the run stops. Cross-checking the resolved company name against the meeting attendees' email domains catches almost all the rest for free.

The second-worst is staleness, and it is hard because it is invisible in every offline eval. A brief generated from a thirty-day-old cache passes verification perfectly, because the quoted spans really do appear in the sources fetched a month ago. The only defence is printing the age of every fact and alerting on the age distribution. If the median fact age in delivered briefs drifts above a threshold, your warm-ahead job is broken or your TTLs are too generous.

Then two failure modes that are legal, not technical. Scraping profile data from platforms whose terms prohibit it is a real risk that teams route around with a commercial enrichment vendor, which moves the liability but does not remove the obligation to know what you store. And processing personal data about EU individuals for sales outreach requires a lawful basis and, in practice, a defensible retention policy. Neither is a reason not to build this. Both are reasons to have the conversation before the pipeline is written, not after a request arrives.

Four signals catch most of the rest: resolution confidence distribution, median fact age in delivered briefs, claim drop rate at verification, and the rep-facing was-this-useful signal. The last is the only direct measure of whether the product works and it costs one button. A brief nobody opens is a brief that failed, and open rate is available on day one without any eval infrastructure.

Failure modeWhat the user seesWhere to fix itDetection signalCost of getting it wrong
Resolved to the wrong companyA fluent, well-cited brief about a different AcmeResolver: confidence threshold plus a deterministic domain cross-check against CRM and attendee emailsResolution confidence distribution; reps flagging wrong-company briefsThe rep opens the call with a fact about someone else. Worst failure in the product
Stale fact presented as currentLast quarter's headcount or a superseded funding roundFreshness: per-source TTL, event-driven invalidation, print the age of every factMedian fact age in delivered briefs; share of facts older than their TTLCredibility loss in the room; no dashboard will ever show it
One slow source stalls the briefThe brief arrives after the meeting startedFan-out: per-source timeout at 4s, partial results are valid, missing sources listedp99 brief latency; per-source timeout rateA late brief has zero value at any quality level
Prompt injection from a fetched pageA brief containing instructions or content the source plantedIngestion: sanitise fetched text, delimit as data, give the synthesis step no toolsInstruction-shaped text detected in fetched content; anomalous brief structureBest case a garbage brief; worst case an agent action driven by a third party
Enrichment returns an unbounded payloadTruncated brief or a context overflow errorTool server: project the fields you need, cap the response, never pass raw vendor JSONTool result size p99; context overflow rateOne wasted run at ~$0.23 and a missing brief
Thin account padded with generic fillerTwo pages of industry commentary and no specificsSynthesis: hard refusal below six verified claims; deliver a note insteadClaims-per-brief distribution; briefs at exactly the minimum lengthReps stop reading briefs entirely. Trust does not come back easily
Two sources disagree and one is picked silentlyA confident number that contradicts what the prospect saysReconciliation: record both with dates and source precedence; surface the conflictConflict rate per field; reps correcting numbers in feedbackThe rep is contradicted by the prospect using the prospect's own data
Enrichment vendor changes or disappearsFirmographics section is empty across all briefsAdapter boundary per source, with a second vendor behind the same interfacePer-source error rate; sudden drop in claims of one categoryClearbit folding into HubSpot as Breeze Intelligence is the worked example
Personal data retained without a basis or policyNothing, until a request or an audit arrivesIngestion and storage: lawful basis recorded, retention policy enforced by a jobRetention job coverage; records past policy ageRegulatory exposure. The one failure on this list that a rewrite cannot fix
A brief that says I could not confirm enough about this account, here are the two questions worth asking is worth more than a confident brief about the wrong company, and costs a fraction of a cent instead of twenty-three.The design principle the refusal path exists to enforce

What does it take to build, and what should you skip?

About six engineer-weeks for a two-person team to a production v1, plus two engineer-days a month of maintenance. Skip the planner, skip multi-agent, skip a vector database, and skip anything that reads a platform's data against its terms. What you cannot skip is the resolver, the TTL table and the verifier.

Week one is the resolver and the journal, and the resolver deserves a full week on its own. Deterministic domain matching, CRM lookup, a confidence score, the refusal path, and a test set of genuinely hard cases: subsidiaries, rebrands, companies with the same name in different countries, and the personal-email attendee whose domain tells you nothing. Every hour here pays back in the failure mode no downstream check catches.

Weeks two and three are the source adapters and the freshness cache. Nine adapters behind one interface, each declaring its TTL, precedence and cold cost, the cache storing values alongside retrieval timestamps. Build the four internal adapters first even though they are less exciting: CRM, notes, telemetry and billing are free, authoritative and the only sources your competitor cannot also buy. Week four is fan-out with per-source timeouts and partial results, plus the map-reduce extraction step.

Week five is synthesis and verification: the typed brief schema, span matching, the fact-versus-inference split and the refusal threshold. Week six is delivery, feedback instrumentation and the warm-ahead batch job. That last item is a half-day that halves your cost per brief and takes several seconds off the p99, and it is nearly always deferred because it does not appear in the demo.

On skipping: the planner is the easiest cut. A fixed plan (always hit these nine sources, always ask these questions) produces briefs almost as good as a planned run, removes a model call and removes an entire class of failure where the planner decides not to check something important. Add planning when you have evidence that different account types need genuinely different research, a real thing but not a v1 thing. Multi-agent is the second cut, for the reasons in multi-agent versus single agent: parallel fan-out over typed adapters is not multi-agent and does not need an agent framework. And for the general pattern for compressing a build like this, building an MVP in days with AI covers the scoping discipline; the ongoing architecture work is what AI product development is for.

Six engineer-weeks, sequenced
  1. Week 1
    Resolver and journal

    Domain to CRM to entity, confidence score, refusal path, and a hard test set: subsidiaries, rebrands, duplicate names across countries, personal-email attendees. Plus the run and step tables and cost attribution.

  2. Weeks 2‑3
    Source adapters and the freshness cache

    Nine adapters behind one interface, each declaring TTL, precedence and cold cost. Values cached with retrieval timestamps. Build the four internal adapters first: they are free, authoritative and uncopyable.

  3. Week 4
    Parallel fan-out and extraction

    Concurrent source calls with a per-source 4s timeout, partial results treated as valid, missing sources listed explicitly. Map-reduce page extraction on a Flash-class model.

  4. Week 5
    Synthesis and verification

    Typed brief schema, deterministic span matching, the fact-versus-inference split, and the refusal threshold below six verified claims. This is the week that decides whether reps trust the output.

  5. Week 6
    Delivery, feedback, warm-ahead

    Slack and calendar delivery with a visible trace id, a one-click useful signal, and the overnight batch job that pre-warms accounts with meetings in the next seven days.

The warm-ahead job in week six is a half-day of work that removes 56% of the cost per brief and several seconds of latency. It is deferred on almost every build because it improves nothing visible in a demo, the same reason the escalation brief gets cut from a support agent.

Building an AI sales research agent: common questions

How much does an AI-generated account brief cost?

About 22.6 cents for a cold brief where all nine sources are fetched, and about 9.8 cents for a warm one where five sources are served from cache. The largest single line is a third-party enrichment record modelled at $0.08, followed by the synthesis model call at $0.045 and web search at $0.042. Those are modelled figures from August 2026 list prices with the token counts and vendor rates printed, not measurements of a deployed system.

How do you keep account research fresh without re-fetching everything?

Assign a TTL per source rather than per cache. Firmographics hold for about thirty days, tech-stack signals for fourteen, job postings for seven, news for six hours, funding announcements for two. Cache the value with its retrieval timestamp and print the age of every fact in the brief. Add event-driven invalidation so a funding story blows away the firmographic cache immediately, and a warm-ahead batch job that pre-fetches slow sources overnight for accounts with meetings in the next week.

Should the research agent be a multi-agent system?

No. The work is a parallel fan-out over typed source adapters with one synthesis step, which is ordinary concurrent code and does not need agents, handoffs or an orchestration framework. Anthropic's own guidance reports multi-agent implementations using roughly three to ten times the tokens of single-agent approaches for equivalent tasks. Running nine source calls with Promise.all and a per-source timeout gives you the parallelism without the token premium or the debugging surface.

What stops the brief from hallucinating facts about an account?

Make the output a typed structure rather than prose. Every claim carries a source URL, a source id, a retrieval timestamp and a quoted span, and verification checks that the span literally appears in the fetched source text: a deterministic substring match, not a model judgement. Claims that fail are deleted rather than hedged. Inferences live in a separate array, must reference the claims they rest on, and render differently so a rep never reads a guess as a fact.

What data sources should a sales research agent use?

Four internal and five external. Internal: CRM account and opportunities, previous call notes, product usage telemetry and billing state, all free, authoritative and refreshed in minutes, and the only sources a competitor cannot also buy. External: firmographic enrichment, tech-stack signals, news, funding and acquisitions, and job postings. Build the internal adapters first even though they are less interesting; a brief that knows what your own telemetry says is one only you can write.

Is scraping LinkedIn or similar platforms for research a problem?

It is a legal question rather than an engineering one, and the answer depends on the platform's terms, the jurisdiction and what you store. Most teams route around it by buying from a commercial enrichment vendor, which moves the liability but does not remove the obligation to know what personal data you hold, on what lawful basis, and for how long. Have that conversation before the ingestion pipeline is written, and enforce retention with a job rather than a policy document.

How long does it take to build a sales research agent?

About six engineer-weeks for a two-person team, plus roughly two engineer-days a month of maintenance. One week on the account resolver and journal, two on source adapters and the freshness cache, one on parallel fan-out and extraction, one on synthesis and verification, and one on delivery, feedback instrumentation and the warm-ahead batch job. Cutting the planner in favour of a fixed research plan removes a model call and a failure class, and is the right trade for version one.

Ready to talk numbers?

Twenty minutes, straight to the engineer. No sales rep, no deck.