A multi-agent orchestrator that produces a pre-call brief for Arvaya account executives before every prospect discovery session. Ships with an iPad-optimized viewer and HTTP/SSE server so Ryan can read briefs and kick off new ones from a tablet.
cp .env.example .env # OPENAI_API_KEY + PERPLEXITY_API_KEY + FIRECRAWL_API_KEY
npm install
npm run serve # boots the server on http://localhost:5174Open http://<your-laptop-ip>:5174 on the iPad (same Wi-Fi as the server). The viewer:
- Lists every brief under
./profiles/in the left sidebar, filterable by company. - Renders the full PublishedBrief as a CTO-friendly dashboard: executive snapshot cards, latest-news cards, role-tagged buying committee, attendee intel, icebreakers + hooks + talking points, risk audit, objection prep, sources. Dark mode by default; light mode kicks in when the iPad is set to light.
- Kicks off new briefs with a "οΌ New brief" button β fill in company / website / prospect / meeting time / recency / objective, hit generate, and the orchestrator's stage progress streams over Server-Sent Events so Ryan watches the brief assemble in real time (~1β3 minutes).
A demo brief for Northwind Logistics ships under ./profiles/northwind-logistics/ β open the viewer immediately to see what a populated brief looks like even before you've configured any API keys.
The sidebar shows every company in ./leads/ grouped by meeting date β Today, This week, Upcoming, Unscheduled, Past. Companies that already have a brief render normally; companies still in the pipeline (no brief yet) surface a "Generate brief" CTA in the detail panel that prefills from the lead's hand-tuned context (archetype, hypothesis, exclude keywords, competitors). The refresh button on an existing brief regenerates from the same lead stub.
npm run seed:leads seeds ./leads/*.json from companies.csv β 40 AECTech 2026 exhibitors with sensible defaults and disambiguation hints for collision-prone names (Box, Mosaic, IES, Kinship, Nomic). Hand-tuned leads (egnyte, joist-ai, seev, Larson-Design-Group) are never clobbered.
- Funding strip in the header β headcount, total funding, last round, founded year, industry. Parsed from the Apollo source's JSON snippet, surfaced before everything else because that's what a CTO scans first.
- Tech stack as chips β pulled from the same Apollo source, so it's visible without reading prose.
- Trust marker (
β) on claims β items with aevidenceQuotethat passed the deterministic verifier get a small green check. Items without (lighter check) still cite a source ID but weren't substring-matched. - Tap-to-copy on icebreakers and talking points β Ryan can paste straight into Slack or a calendar invite.
- Hash routing β every brief has a stable URL (e.g.
#brief:egnyte/2026-05-13T16-36-11-783Z/brief.json). Bookmarkable on the iPad home screen; back/forward gestures work. - Skeleton loaders while a brief loads β no flash of empty state.
- Live SSE progress in the modal when a new brief is being generated; sidebar row badges flip to "generatingβ¦" with a pulse animation so Ryan can tell at a glance.
| Method | Path | What it does |
|---|---|---|
GET |
/ |
Serves viewer/index.html. |
GET |
/api/briefs |
Lists every brief in profilesDir, newest first. |
GET |
/api/briefs/<slug>/<runId>/brief.json |
Returns one PublishedBrief. |
POST |
/api/briefs |
Accepts a Lead, starts the pipeline in-process, returns { runId } immediately. |
GET |
/api/briefs/<runId>/events |
SSE stream: stage_started, stage_finished, done, failed. |
You hand the system a prospect (company, AE, meeting time) as JSON. The system:
- Discovers the prospect's digital footprint via Perplexity Sonar deep research β real-time news, exec interviews, job postings, financials, and buying-committee intelligence
- Extracts deep content from high-value URLs via Firecrawl β full interview transcripts, earnings call pages, engineering blog posts
- Extracts strategic initiatives and pain points from the gathered sources
- Detects risk signals β layoffs, budget freezes, leadership churn, security incidents, legal issues
- Writes a personalized brief: TL;DR, Call Objective, Icebreakers, Value Alignment Hooks, Potential Red Flags, Talking Points, Attendee Intel, Objection Predictions
- Verifies every claim against captured source snippets (recency, deduplication, specifics checks)
- Writes the brief and all supporting research into a company profile folder on disk
Five specialized OpenAI agents pipelined by a thin TypeScript orchestrator. Each agent is a single chat.completions.create call to gpt-4o via the official openai SDK with its own system prompt, tools, and strictly typed JSON output. The orchestrator owns the data plane β it threads outputs from one agent into the next and persists everything to a per-prospect profile folder.
SDK choice. We use the OpenAI Chat Completions API directly. The Researcher's tool calls are dispatched locally; OpenAI does not offer a server-side
web_searchtool, so all retrieval is done through Perplexity (sonar-profor breadth,sonar-deep-researchfor depth) and Firecrawl. The downstream four agents are tool-free: closed-book reasoning over what the Researcher captured.
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Orchestrator (Node) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββ
β β β
βΌ βΌ βΌ
Lead JSON Five-agent pipeline ./profiles/<slug>/<runId>/
(file or CLI flags) (OpenAI SDK) brief.md + research/*.json
β
ββββββββββββββ¬ββββββββββββββββββββββββββββββΌβββββββββββββββββββββββ¬βββββββββββββββββββββ
βΌ βΌ βΌ βΌ βΌ
Researcher SignalExtractor RiskDetector PersonalizationWriter QAVerifier
β
βββββββββββββββββββββΌββββββββββββββββββββββββββββββββββ
βΌ βΌ βΌ
perplexity_search perplexity_discovery firecrawl_scrape
(custom tool) (custom tool) (custom tool)
Perplexity sonar-pro Perplexity sonar-deep-research Firecrawl
β β β β β
βΌ βΌ βΌ βΌ βΌ
SourcePack Signals Risks DraftBrief VerifiedBrief
β
βΌ
Profile folder on disk
A pipeline gives us:
- Deterministic separation of concerns. The Researcher only fetches; the Verifier only checks.
- Independent prompt tuning. Risk detection and personalization are different muscles; we iterate on them separately.
- Cheap, parallel-safe verification. The QA agent sees only the draft and cached source snippets β no tools, no re-querying β so it cannot hallucinate a fix.
- Structured handoffs. Each stage emits typed JSON, which we persist. Reproducible, debuggable, and easy to swap any one agent later.
| Agent | Input | Tools | Output |
|---|---|---|---|
| Researcher | Lead |
perplexity_search, perplexity_discovery, firecrawl_scrape |
SourcePack |
| SignalExtractor | SourcePack |
none | Signals |
| RiskDetector | SourcePack |
none | Risks |
| PersonalizationWriter | Lead, SourcePack, Signals, Risks |
none | DraftBrief |
| QAVerifier | DraftBrief, SourcePack |
none | VerifiedBrief |
Citations use opaque sourceIds assigned by the Researcher (e.g. src-3). Every downstream claim must cite at least one. The Verifier rejects any claim whose snippet does not actually support it.
| Tool | Layer | Why |
|---|---|---|
Perplexity perplexity_search |
Breadth scan (cheap) | sonar-pro does fast, ~$0.01/call landscape mapping. The Researcher runs this first to decide which angles have enough signal to warrant a deep-research call. |
Perplexity perplexity_discovery |
Targeted deep research | sonar-deep-research does multi-step web reasoning to find specific interviews, job postings, news, financials, and people. ~$0.05β$0.10/call, 60β120s per call. Capped at 2β3 calls per run, only on angles confirmed by the breadth scan. |
Firecrawl firecrawl_scrape |
Full-page extraction | Used sparingly on high-value URLs where the Perplexity snippet is too thin to support a defensible claim β exec interview transcripts, earnings call pages, engineering blog posts. Capped at 5 fetches per run. |
| Section | Purpose |
|---|---|
| TL;DR | 1β3 bullets for the AE who has 2 minutes, not 10 |
| Call Objective | One-sentence north star: the single most important thing to learn |
| Icebreakers | Specific, time-bound openers tied to concrete sources; ordered by recency |
| Value Alignment Hooks | Connects Arvaya's wedge to each major initiative; frameable against productFocus |
| Potential Red Flags | Risk signals the AE needs to know going in |
| Talking Points | Open-ended pain-discovery questions, each source-backed |
| Attendee Intel | Buying committee and likely call participants beyond the primary prospect |
| Objection Predictions | Context-specific objections with suggested responses |
| Sources | Full citation list with URLs and dates |
- OpenAI gpt-4o (5 agents): $0.15β$0.40
- Perplexity (1β2
sonar-probreadth + 2β3sonar-deep-researchdeep): ~$0.15β$0.40 - Firecrawl: ~$0.005 Γ β€5 fetches = ~$0.025
Total: roughly $0.30β$0.80 per prospect brief.
Every successful run writes a single self-contained folder:
profiles/
βββ northwind-logistics/
βββ 2026-05-07T20-15-22-117Z/
βββ brief.md # β the deliverable, read this first
βββ run.json # lead, timing, status (includes the Lead you provided)
βββ research/
βββ sources.json # Researcher output: SourcePack
βββ signals.json # SignalExtractor output
βββ risks.json # RiskDetector output
βββ draft-brief.json # PersonalizationWriter output
βββ verified-brief.json # QAVerifier output (final, pruned)
Multiple runs for the same prospect over time stack neatly under the company slug. The CLI prints the profile folder path and brief path on success.
Provide the lead as JSON. Two ways:
Lead file (recommended):
cp leads/_template.json leads/<company-slug>.json
# edit leads/<company-slug>.json β replace every <...> placeholder
npm run prep -- --lead leads/<company-slug>.jsonleads/_template.json is the canonical scaffold. Sensible defaults are pre-filled (AE name, AE email, productFocus, archetype, meetingType); per-prospect fields carry <...> placeholders that double as inline guidance. samples/exampleLead.json is a fully-populated worked example you can compare against.
Three pre-built validation leads also live in leads/ β egnyte.json, joist-ai.json, seev.json. They span the failure modes (rich footprint, severe name-collision, thin footprint) and are the recommended sanity check after any change to the Researcher prompt.
Inline flags:
npm run prep -- \
--company "Northwind Logistics" \
--website https://northwindlogistics.com \
--ae-name "Jordan" \
--ae-email jordan@arvayaconsulting.com \
--prospect-name "Pat Lee" \
--prospect-title "VP Data" \
--meeting-at 2026-05-12T15:00:00Z \
--product-focus "RAG implementation" \
--deal-stage warm \
--prospect-archetype other \
--meeting-type first_intro \
--intro-source cold_outbound \
--call-objective "Confirm whether the Q3 self-service analytics launch is funded." \
--hypothesis "Their data org is fragmented; they need orchestration before models." \
--exclude-keywords "northwind database,sql server example" \
--competitive-context "Slalom,Snowflake Professional Services"The bare
--before your flags tells npm to pass everything to the script. Flags with multiple values (--exclude-keywords,--competitive-context) accept comma-separated strings.
Lead JSON shape β every field except company, aeName, and aeEmail is optional. Open leads/_template.json for the full annotated scaffold; here is the abbreviated map:
Required fields: company, aeName, aeEmail.
Optional but high-impact (basics): prospectTitle (drives title calibration in the brief), productFocus (sharpens value alignment hooks), dealStage (tunes objection framing), prospectArchetype (one of aec_firm, aec_vendor, other β switches the Researcher's tool plan; see below), excludeKeywords (string array β disambiguation hints; sources whose URL/title/snippet contain any of these are dropped).
Optional goal/context fields (substantially improve brief quality when populated):
| Field | What it does |
|---|---|
callObjective |
What Arvaya wants out of THIS specific meeting. The Writer uses it verbatim instead of synthesizing one. One sentence is enough. |
hypothesis |
Arvaya's prior on the prospect's biggest pain. The Researcher tests it (validate or contradict via a dedicated breadth-scan call); the Writer leads with it if confirmed and soft-pivots if disconfirmed. |
meetingContext |
Free-form narrative of how the meeting came about β referral, event, inbound, etc. Used to set tone but never quoted directly in the brief. |
meetingType |
One of first_intro, discovery, proposal_review, renewal, partnership_explore. Adjusts the brief shape β first_intro is exploratory; proposal_review is objection-heavy; renewal is retention-framed. |
engagementShape |
What Arvaya is selling on this call. Free-form, e.g. "fixed-scope discovery sprint", "advisory retainer", "implementation engagement". AT LEAST ONE Value Alignment Hook must reference it directly when set. |
introSource |
One of inbound, referral, event, cold_outbound, reactivation. Picks icebreaker register. |
introContext |
Free-form qualifier on introSource, e.g. "Met at AECTech booth" or "Referred by Pat Lee at Northwind". |
competitiveContext |
String array of likely competing vendors. Every named competitor must appear in at least one Objection Prediction. |
The big three fields with the largest measurable impact on brief quality are callObjective, hypothesis, and meetingContext. Populate those before worrying about the rest.
| Value | When | What changes |
|---|---|---|
aec_firm |
General contractors, design firms, owner-operators β companies that build things or run AEC projects | Researcher runs SAM.gov / USASpending / IDIQ / task-order queries plus a Procore / Autodesk / Bluebeam / Viewpoint stack scan |
aec_vendor |
Software, SaaS, or services vendors selling INTO AEC | Researcher runs vendor-shape queries: AEC customer case studies, ML/data-engineering hiring, GitHub presence, funding history; SKIPS the gov-contract/project-stack queries |
other (default) |
Everything else | General-purpose flow with no archetype-specific calls |
Use whenever the company name collides with a bigger or different entity. Without it, the Researcher silently pulls wrong-company sources. Example values:
- For
Box(the cloud-content company):["box office", "cardboard", "boxing"] - For
Joist AI:["Joist app", "consumer contractor", "homeowner"] - For
Brantley Construction Company:["Brantley County wildfire", "Brantley County, GA"]
Crawler2/
βββ package.json
βββ tsconfig.json
βββ .env.example
βββ README.md
βββ companies.csv # Target-account list (40 AEC vendors)
βββ arvaya-lead.json # Self-test lead (Arvaya researching itself)
βββ docs/
β βββ architecture.svg # Visual schematic
β βββ build-summary.md # Decisions log + file inventory
β βββ audit-2026-05-08.md # Crawler audit + tier-by-tier company plan
βββ leads/ # β Per-prospect lead JSONs go here
β βββ _template.json # Annotated scaffold β copy this for new leads
β βββ egnyte.json # Validation lead β Tier A (rich footprint)
β βββ joist-ai.json # Validation lead β Tier B (name-collision)
β βββ seev.json # Validation lead β Tier C (thin footprint)
βββ src/
β βββ index.ts # CLI: reads lead.json or flags
β βββ server.ts # HTTP + SSE server (npm run serve) β NEW 2026-05-15
β βββ config.ts # Env loader + validation
β βββ types.ts # Lead, SourcePack, Signals, Risks, Brief, ...
β βββ orchestrator.ts # Pipelines the 5 agents; emits structured progress events
β βββ agents/
β β βββ researcher.ts
β β βββ signalExtractor.ts
β β βββ riskDetector.ts
β β βββ personalizationWriter.ts
β β βββ qaVerifier.ts
β βββ prompts/
β β βββ researcher.md # Phase -1 disambiguation, archetype branching, soft minimum
β β βββ signalExtractor.md
β β βββ riskDetector.md
β β βββ personalizationWriter.md # Reads goal/context fields off Lead + latestNews/buyingCommittee rules
β β βββ qaVerifier.md
β βββ tools/
β β βββ apollo.ts # Firmographics + people search (Phase 0)
β β βββ perplexity.ts # perplexity_search (sonar-pro) + perplexity_discovery (sonar-deep-research)
β β βββ firecrawl.ts # URL β clean markdown
β β βββ email.ts # Resend wrapper (opt-in: lead.deliverByEmail)
β βββ lib/
β βββ agentClient.ts # Shared runAgent β wraps OpenAI Chat Completions + tool loop
β βββ briefRenderer.ts # VerifiedBrief β markdown + PublishedBrief JSON
β βββ verify.ts # Deterministic substring/quote verification
β βββ jsonExtract.ts # Robust JSON extraction from agent output
β βββ costLedger.ts # Per-tool cost telemetry
β βββ logger.ts # JSON logger
βββ viewer/
β βββ index.html # Single-file iPad-optimised viewer β NEW 2026-05-15
βββ samples/
β βββ exampleLead.json # Worked example β fully populated current-schema lead
β βββ exampleBrief.md # Reference brief output for prompt tuning
βββ profiles/ # β generated; one folder per prospect run
Two entry points share the same orchestrator:
CLI β one-shot per lead, writes to disk:
cp .env.example .env # OPENAI_API_KEY + PERPLEXITY_API_KEY + FIRECRAWL_API_KEY (+ optional APOLLO_API_KEY)
npm install
cp leads/_template.json leads/my-lead.json # edit per-prospect fields
npm run prep -- --lead leads/my-lead.json # generates brief.md + brief.jsonHTTP server + iPad viewer β multi-brief UI for Ryan:
npm run serve # http://localhost:5174Then open the URL on the iPad. Generate new briefs from the "οΌ New brief" button; the SSE progress stream lights up each stage as the orchestrator runs.
npm test runs the 65 hermetic tests (no API keys needed). npm run typecheck runs the TS compiler in noEmit mode.
This pass landed Phase 1F (schema additions) and the bulk of Phase 2B (frontend integration) from docs/roadmap.md:
latestNewsandbuyingCommitteeare first-class schema sections.BriefItem.evidenceQuoteis verified by the deterministic pre-pass (was Risks-only before).Lead.recencyandSourcePack.freshnessflow end-to-end.DraftBriefminimum-count floors softened so the Writer can degrade gracefully on thin-footprint companies.- Orchestrator progress events flow through a typed
EventEmitterconsumed by both the CLI spinner and the SSE endpoint. src/server.ts+viewer/index.htmlship the iPad-friendly UI end-to-end with a demo brief pre-populated under./profiles/northwind-logistics/.
{ // --- Required --- "company": "Northwind Logistics", "aeName": "Jordan", "aeEmail": "jordan@arvayaconsulting.com", // --- Basics (optional, recommended) --- "website": "https://northwindlogistics.com", "prospectName": "Pat Lee", "prospectTitle": "VP Data", "meetingAt": "2026-05-12T15:00:00Z", "productFocus": "RAG implementation", "dealStage": "cold | warm | evaluation", "prospectArchetype": "aec_firm | aec_vendor | other", "excludeKeywords": ["disambiguation", "hints"], // --- Goal / context (optional, big lift on quality) --- "callObjective": "...", "hypothesis": "...", "meetingContext": "...", "meetingType": "first_intro | discovery | proposal_review | renewal | partnership_explore", "engagementShape": "...", "introSource": "inbound | referral | event | cold_outbound | reactivation", "introContext": "...", "competitiveContext": ["competitor 1", "competitor 2"] }