--- title: Competitor Briefing Agent | Tabstack description: 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. --- ## What you get back ``` { 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. --- ## Complete example - [TypeScript](#tab-panel-0) - [Python](#tab-panel-1) ``` 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 { 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 } } ``` ``` from datetime import datetime, timezone from tabstack import Tabstack, RateLimitError, AuthenticationError client = Tabstack() PRICING_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", }, }, }, }, }, } def run_research(query: str) -> dict: for event in client.agent.research(query=query, mode="fast"): if event.event == "error": msg = getattr(event.data.error, "message", None) or "unknown error" raise RuntimeError(msg) if event.event == "complete": cited = event.data.metadata.cited_pages or [] return { "answer": event.data.report, "sources": [{"title": p.title, "url": p.url} for p in cited], } raise RuntimeError("Stream ended without a complete event") def build_competitor_briefing(company: str, pricing_url: str) -> dict: pricing = client.extract.json(url=pricing_url, json_schema=PRICING_SCHEMA) research = run_research( f"What has {company} shipped, announced, or changed in the last 90 days? " "Include pricing changes, product launches, and API updates." ) return { "pricing": pricing, "recent_signals": research["answer"], "sources": research["sources"], "retrieved_at": datetime.now(timezone.utc).isoformat(), } try: briefing = build_competitor_briefing("Acme Corp", "https://acmecorp.example/pricing") print("Pricing plans:") for plan in briefing["pricing"].get("plans", []): price = plan.get("price") print(f" {plan['name']}: ${price if price is not None else 'custom'}/mo") print("\nRecent signals:") print(briefing["recent_signals"]) sources = briefing["sources"] print(f"\nSources ({len(sources)}):") for s in sources: print(f" {s['title']}: {s['url']}") except RateLimitError: print("Rate limit -- retry after a pause") except AuthenticationError: print("Invalid API key -- check TABSTACK_API_KEY") ``` --- ## How it works `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. --- ## Extending this pattern 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. --- ## Installation Terminal window ``` npm install @tabstack/sdk # TypeScript pip install tabstack # Python export TABSTACK_API_KEY="your-key-here" ```