Small businesses can't afford dedicated market research teams, yet they need timely competitive intelligence to stay ahead. Manual tracking of news, pricing, and product changes is overwhelming.
A complete, working implementation of this recipe — downloadable as a zip or browsable file by file. Generated by our build pipeline; tested with full coverage before publishing.
In this tutorial you’ll build a Perplexity Agent Mesh for SMB Competitive Intelligence — a multi-agent system that monitors competitors, tracks pricing changes, and surfaces actionable market insights. The mesh uses Perplexity for web-grounded research, LangGraph for workflow orchestration, and a suite of REAA packages for agent routing, classification, session management, and cost tracking. By the end you’ll have a Next.js app with three API endpoints and a dashboard that shows your competitive intelligence mesh in action.
A Google Cloud project with Vertex AI enabled (for the classifier — the test suite uses a mock, but production needs real GCP credentials)
Basic familiarity with TypeScript and Next.js App Router patterns
Step 1: Scaffold the Project and Configure Environment Variables
Start by creating a Next.js 16 project with the App Router. The scaffold produces next.config.ts, tsconfig.json, and the base dependency tree. You’ll add the remaining config files and dependencies in this step.
Create .env.example and add the environment variables the mesh needs:
env
# Env vars used by perplexity-agent-mesh-for-smb-competitive-intelligence.# The builder adds entries here as it wires up each integration.# Keep placeholders only — never commit real values.NODE_ENV=development# Perplexity APIPERPLEXITY_API_KEY=<your-perplexity-key># GCP for agent-mesh-classifier Gemini Flash on Vertex AIGOOGLE_CLOUD_PROJECT=<your-gcp-project>GOOGLE_CLOUD_LOCATION=us-central1GOOGLE_GENAI_USE_VERTEXAI=trueGOOGLE_APPLICATION_CREDENTIALS=</path/to/service-account.json># Session management (agent-mesh-session)SESSION_TTL_MINUTES=30SESSION_MAX_TURNS=100# MCP dispatch (agent-mesh-router)MCP_REQUEST_TIMEOUT_MS=30000MCP_MAX_RETRIES=3# Cost tracking (llm-cost-telemetry)DEFAULT_DAILY_BUDGET=5.00# AppNEXT_PUBLIC_APP_URL=http://localhost:3000
Expected output: Your package.json dependencies section should show exact pinned versions with no ^ or ~ prefixes. The .env.example file should list all 12 environment variables above.
Step 2: Define Shared Types and Schemas
The mesh needs shared domain types that describe competitors, intelligence items, agent roles, and API shapes. Create src/types/index.ts:
ts
import { IncomingRequestSchema, type IncomingRequest, AgentResponseSchema, type AgentResponse, ContextPacketSchema, type ContextPacket, ClassifierOutputSchema, type ClassifierOutput, SessionRecordSchema, type SessionRecord, TurnEntrySchema, type TurnEntry, AgentConfigSchema, type AgentConfig, ConfidenceDecisionSchema, type ConfidenceDecision, SERVICE_NAME, CONFIDENCE, SESSION } from "@reaatech/agent-mesh";export type IntelCategory = "pricing" | "product" | "news" | "market" | "partnership";export interface CompetitorProfile { id: string; name: string; website: string; industry: string; lastScrapedAt: string;}export interface IntelItem { id: string; competitorId: string; category: IntelCategory; title: string; summary: string; sourceUrl: string; sourceType: string; detectedAt: string; confidence: number;}export type AgentRole = "competitor-tracker" | "price-monitor" | "news-summarizer";export interface MeshQuery { query: string; competitorIds?: string[]; categories?: IntelCategory[]; timeRange?: string;}export interface MeshResponse { intel: IntelItem[]; sessionId: string; cost: number; budgetRemaining: number;}export interface WebhookPayload { source: string; payload: unknown; contentType: string; receivedAt: string;}// Re-exports from @reaatech/agent-meshexport { IncomingRequestSchema, type IncomingRequest, AgentResponseSchema, type AgentResponse, ContextPacketSchema, type ContextPacket, ClassifierOutputSchema, type ClassifierOutput, SessionRecordSchema, type SessionRecord, TurnEntrySchema, type TurnEntry, AgentConfigSchema, type AgentConfig, ConfidenceDecisionSchema, type ConfidenceDecision, SERVICE_NAME, CONFIDENCE, SESSION };
Expected output: The file exports 7 custom interfaces/types (IntelCategory, CompetitorProfile, IntelItem, AgentRole, MeshQuery, MeshResponse, WebhookPayload) plus re-exports all Zod schemas and constants from @reaatech/agent-mesh.
Step 3: Create the Perplexity API Adapter
All Perplexity calls flow through a single adapter class. This keeps the rest of your code decoupled from the SDK. Create src/lib/perplexity.ts:
Expected output: The adapter provides three methods — chat() for arbitrary messages, search() for single-query lookups, and searchWithContext() for system-prompt-guided research.
Step 4: Add Cost Tracking with Budget Enforcement
To keep Perplexity API spend under control, wrap the adapter in a CostGuard that records every call’s token usage and enforces a daily budget. Create src/lib/costGuard.ts:
Expected output: The CostGuard wraps every Perplexity call with token-usage tracking and stores CostSpan records in an in-memory ring buffer. checkBudget() throws when the daily spend exceeds the cap.
Step 5: Build the Competitor Tracker Agent
The first specialist agent researches competitors by querying Perplexity for news, product launches, and changes. Create src/agents/competitor-tracker.ts:
Expected output: The agent checks budget before each search, returns parsed IntelItem[] arrays, and gracefully returns an empty array when Perplexity returns no content.
Step 6: Build the Price Monitor Agent
The price monitor searches for pricing changes and plans. Create src/agents/price-monitor.ts:
Expected output: The agent tags all returned items with category: "pricing" and, when given a baseline, appends pricing comparison context to each summary.
Step 7: Build the News Summarizer Agent
The news summarizer searches for industry and competitor news, emitting lifecycle events as it runs. Create src/agents/news-summarizer.ts:
Expected output: The agent exposes a public events: TypedEventEmitter that fires "summarize:start", "summarize:done", and "summarize:error" events during processing.
Step 8: Wire the Mesh Hub with LangGraph Orchestration
The MeshHub is the central orchestrator. It initializes the cost guard, confidence router, all three specialist agents, builds a LangGraph StateGraph pipeline, and exposes processQuery() and processWebhook() entry points. Create src/mesh/hub.ts:
ts
import { StateGraph, MessagesAnnotation } from "@langchain/langgraph";import { classifierService } from "@reaatech/agent-mesh-classifier";import { createSession, getActiveSession, appendTurn, closeSession, updateWorkflowState } from "@reaatech/agent-mesh-session";import { dispatchToAgent, shouldCloseSession, getUpdatedWorkflowState } from "@reaatech/agent-mesh-router";import { ConfidenceRouter } from "@reaatech/confidence-router";import type { AgentConfig, AgentResponse, ClassifierOutput, SessionRecord } from "../types/index.js";import { CostGuard } from "../lib/costGuard.js";import { PerplexityClient } from "../lib/perplexity.js";import { CompetitorTrackerAgent } from "../agents/competitor-tracker.js";import { PriceMonitorAgent } from "../agents/price-monitor.js"
Expected output: The MeshHub constructor initializes all dependencies. processQuery() orchestrates session lookup, intent classification, agent routing via dispatchToAgent, and session lifecycle management. getMeshStatus() returns the registered agent count and daily spend.
Step 9: Create the Webhook Handler
The webhook handler ingests external triggers — RSS feeds, Slack messages, or API calls — validates them with IncomingRequestSchema, and submits them to the mesh. Create src/api/webhooks.ts:
Expected output: The handler validates incoming payloads against the Zod schema, parses RSS XML with regex-based extraction, and processes payloads sequentially to avoid concurrent Perplexity API calls.
Step 10: Add API Route Handlers
Three API routes expose the mesh: /api/query (POST), /api/webhook (POST), and /api/status (GET).
import { type NextRequest, NextResponse } from "next/server";import { WebhookHandler } from "../../../src/api/webhooks.js";import { MeshHub } from "../../../src/mesh/hub.js";const meshHub = new MeshHub();const webhookHandler = new WebhookHandler(meshHub);export async function POST(req: NextRequest) { try { const body: unknown = await req.json(); const result = await webhookHandler.ingestWebhook(body, "webhook"); return NextResponse.json({ ok: true, result }, { status: 200 }); } catch (error) { if (error instanceof Error && "issues" in error) { return NextResponse.json({ error: "invalid payload", details: error.issues }, { status: 400 }); } return NextResponse.json({ error: "internal error" }, { status: 500 }); }}
Create app/api/status/route.ts:
ts
import { NextResponse } from "next/server";import { MeshHub } from "../../../src/mesh/hub.js";const meshHub = new MeshHub();export function GET() { const status = meshHub.getMeshStatus(); return NextResponse.json({ status: "ok", sessionsActive: status.sessionsActive, costToDate: status.costToDate, agentsRegistered: status.agentsRegistered });}
Expected output: You now have three routes. POST /api/query accepts { "query": "track Acme Corp" } and returns intel with cost data. POST /api/webhook validates with the Zod schema. GET /api/status reports mesh health.
Step 11: Add Middleware for Request IDs
Every API request should carry a unique trace ID. Create middleware.ts at the project root (next to next.config.ts, not inside app/):
ts
import { type NextRequest, NextResponse } from "next/server";import { nanoid } from "nanoid";export function middleware(_req: NextRequest) { void _req; const response = NextResponse.next(); response.headers.set("X-Request-Id", nanoid()); return response;}export const config = { matcher: ["/api/:path*"],};
Expected output: All API responses include an X-Request-Id header. Non-API paths (/favicon.ico, static assets) are skipped by the matcher.
Step 12: Build the Dashboard UI Page
Replace the scaffolded placeholder page with a dashboard that fetches status from the mesh. Edit app/page.tsx:
Expected output: The dashboard loads at /, fetches /api/status on mount, and displays three cards: registered agent count, today’s cost, and active sessions.
Step 13: Write and Run Tests
The test suite uses Vitest with v8 coverage, MSW to mock the Perplexity API, and vi.mock to stub the REAA packages that require GCP credentials.
First, create vitest.config.ts at the project root to configure Vitest and define coverage thresholds:
import { setupServer } from "msw/node";import { http, HttpResponse } from "msw";import { vi, beforeAll, afterEach, afterAll } from "vitest";// Set env vars in hoisted scope so they're set before any module importsvi.hoisted(() => { process.env.GOOGLE_CLOUD_PROJECT = "test-project"; process.env.PERPLEXITY_API_KEY = "test-key"; process.env.API_KEY = "test-api-key";});// Mock perplexity-sdk — the package has a broken exports field// The mock makes real HTTP fetch calls so MSW can intercept and override per-testvi.mock("perplexity-sdk", () => { return { default: class {
Create tests/types.test.ts to validate the Zod schemas:
Run the full test suite to verify everything works:
terminal
pnpm typecheckpnpm lintpnpm vitest run --coverage --reporter=json --outputFile=vitest-report.json
Expected output:typecheck exits with zero errors, lint passes, and the test runner reports all tests passing with line/branch/function/statement coverage at or above 90%.
Next steps
Add more specialist agents — extend the mesh with a social-media monitor, patent-tracker, or regulatory-change agent, each following the same CostGuard + HandoffConfig pattern
Replace in-memory cost tracking with the OTel exporter from @reaatech/llm-cost-telemetry-exporters to push CostSpan records to CloudWatch or GCP Cloud Monitoring
Add real GCP credentials and swap the mock classifier for live Vertex AI Gemini Flash classification using the GOOGLE_APPLICATION_CREDENTIALS env var from .env.example
Build a recurring cron pipeline that polls RSS feeds via ingestRssFeed() daily and emails a competitive-intelligence digest
;
import { NewsSummarizerAgent } from "../agents/news-summarizer.js";
import type { MeshResponse, WebhookPayload } from "../types/index.js";