Skip to content
Get started

Competitor Briefing Agent

Extract structured pricing from a competitor's page and research their recent announcements. A complete working example combining /extract/json and /research.

Pull a complete competitor profile: schema-validated pricing extracted directly from their page, plus recent signals synthesized from across the web. In TypeScript, both calls run in parallel.


{
pricing: {
plans: [
{ name: 'Starter', price: 29, features: ['5 seats', '10k API calls/mo'] },
{ name: 'Pro', price: 99, features: ['25 seats', '100k API calls/mo', 'priority support'] },
{ name: 'Enterprise', price: null, features: ['custom seats', 'SLA'] },
]
},
recentSignals: "Acme Corp launched a batch processing API in Q1 2026...",
sources: [
{ title: "Acme Corp Blog: Q1 Updates", url: "https://blog.acmecorp.example/q1-2026" },
// ...
],
retrievedAt: "2026-04-15T12:00:00.000Z"
}

pricing comes from schema-validated extraction on the pricing page. recentSignals and sources come from multi-source research. Neither requires prompt engineering or manual parsing.

Note: Replace https://acmecorp.example/pricing with your competitor’s real pricing page.


import Tabstack, { RateLimitError, AuthenticationError } from '@tabstack/sdk'
const client = new Tabstack()
interface PricingPlan {
name: string
price: number | null
features: string[]
}
interface CompetitorBriefing {
pricing: { plans: PricingPlan[] }
recentSignals: string
sources: Array<{ title: string; url: string }>
retrievedAt: string
}
async function runResearch(query: string) {
const stream = await client.agent.research({ query, mode: 'fast' })
for await (const event of stream) {
if (event.event === 'error') {
throw new Error(event.data.error?.message ?? 'unknown error')
}
if (event.event === 'complete') {
return {
answer: event.data.report,
sources: (event.data.metadata.citedPages ?? []).map((p) => ({
title: p.title ?? '',
url: p.url,
})),
}
}
}
throw new Error('Stream ended without a complete event')
}
async function buildCompetitorBriefing(
company: string,
pricingUrl: string,
): Promise<CompetitorBriefing> {
const [pricing, research] = await Promise.all([
client.extract.json({
url: pricingUrl,
json_schema: {
type: 'object',
properties: {
plans: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Plan name' },
price: {
type: ['number', 'null'],
description: 'Monthly price in USD, null if custom or contact-us',
},
features: {
type: 'array',
items: { type: 'string' },
description: 'Key included features',
},
},
},
},
},
},
}),
runResearch(
`What has ${company} shipped, announced, or changed in the last 90 days? ` +
`Include pricing changes, product launches, and API updates.`
),
])
return {
pricing: pricing as { plans: PricingPlan[] },
recentSignals: research.answer,
sources: research.sources,
retrievedAt: new Date().toISOString(),
}
}
try {
const briefing = await buildCompetitorBriefing(
'Acme Corp',
'https://acmecorp.example/pricing',
)
console.log('Pricing plans:')
for (const plan of briefing.pricing.plans) {
console.log(` ${plan.name}: $${plan.price ?? 'custom'}/mo`)
}
console.log('\nRecent signals:')
console.log(briefing.recentSignals)
console.log(`\nSources (${briefing.sources.length}):`)
for (const s of briefing.sources) {
console.log(` ${s.title}: ${s.url}`)
}
} catch (err) {
if (err instanceof RateLimitError) {
console.error('Rate limit -- retry after a pause')
} else if (err instanceof AuthenticationError) {
console.error('Invalid API key -- check TABSTACK_API_KEY')
} else {
throw err
}
}

buildCompetitorBriefing runs two calls in parallel via Promise.all:

  • client.extract.json() fetches the pricing page and returns a schema-validated object. The schema defines exactly what fields to pull. No HTML parsing, no post-processing.
  • client.agent.research() runs multi-source research across the web and streams events. runResearch consumes the stream and resolves on the complete event, returning the synthesized answer and cited sources.

Total latency is the slower of the two, not their sum.

The Python version runs them sequentially rather than concurrently — use concurrent.futures.ThreadPoolExecutor if parallel execution matters in your context.


The same structure applies anywhere you need both a structured snapshot and broader context:

  • Prospect research: extract a company’s job postings schema + research their recent funding news
  • Market monitoring: extract a product’s changelog entries + research developer sentiment
  • Due diligence: extract financials from a known filing URL + research recent press coverage

Swap the schema and query. The agent structure stays the same.


Terminal window
npm install @tabstack/sdk # TypeScript
pip install tabstack # Python
export TABSTACK_API_KEY="your-key-here"