# SaaS Competitor Battlecards (`entranced_gelato/saas-competitor-battlecards`) Actor

Generate sales-ready competitor battlecards from public pricing & feature pages. Cited sources, deterministic, MCP-ready. $1 per battlecard.

- **URL**: https://apify.com/entranced\_gelato/saas-competitor-battlecards.md
- **Developed by:** [AIDevs](https://apify.com/entranced_gelato) (community)
- **Categories:** Business, AI, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, NaN bookmarks
- **User rating**: No ratings yet

## Pricing

from $1,000.00 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

## SaaS Competitor Battlecards

**Generate sales-ready competitor battlecards from public pricing and feature pages — in JSON, markdown, and one-pager formats.**

This Actor finds the official website, pricing page, and features page for any SaaS company you name, extracts the structured pricing tiers and feature list, and assembles them into a battlecard your sales team can take into a deal. Every fact cites a source URL. Nothing is fabricated.

> **Disclaimer.** Output is research material, not authoritative pricing or product information. Every fact is paired with a source URL and must be verified before use in customer-facing materials. Vendor names are used nominatively to identify products; no affiliation or endorsement is implied. Provided AS IS, without warranty. Full Terms of Use are summarized at the bottom of this README.

---

### Who this is for

- **Account executives** prepping a competitive deal in 10 minutes instead of an hour.
- **Solutions consultants / presales engineers** building objection-handling notes ahead of a demo.
- **Product marketing** maintaining battlecards across a long competitor set.
- **Founders** running competitive analysis without hiring an SDR.
- **AI agents and LLM tools** — the Actor is exposed via [Apify MCP](https://docs.apify.com/platform/integrations/mcp), so Claude, Cursor, ChatGPT, and any MCP-aware client can call it as a structured tool.

---

### What you get

For each run, a single dataset record with three views of the same battlecard:

1. **Structured JSON** — the source of truth. 14 top-level fields including pricing tiers, feature comparison matrix, positioning, strengths/weaknesses, objection handling, win/loss guidance, risk flags, and citation list.
2. **Markdown summary** — copy-paste into Notion, Confluence, or a Slack message.
3. **One-pager text** — terminal-style brief sized for a single PDF page.

#### Sample one-pager

````

# BATTLECARD — Linear vs Asana

Mode: head\_to\_head | Status: ok | Confidence: 86/100

## PRICING AT A GLANCE

Linear: USD 8.0/monthly | free tier ✓
Asana:  USD 10.99/monthly | free tier ✓ | contact-sales ✓

## CAPABILITIES SIGNAL (top promoted, by company)

Linear: Issue tracking, Cycles, Roadmaps, Triage, Linear Asks
Asana:  Tasks, Projects, Workflows, Goals, Reporting

## WHY WE WIN / WHY WE LOSE

Asana:
\+ Cheaper entry tier than ours
\- No transparent enterprise pricing — slow procurement

## TOP OBJECTIONS

Objection: "Asana has a free tier — why pay for yours?"
Response : Acknowledge the free tier; reframe to TCO and outcomes.

````

---

### How to use it

#### From the Apify console (no code)

1. Click **Try for free** (or **Start**, if you've used it before).
2. In the *Input* panel, enter:
   - **Mode**: `head_to_head` (your product vs one competitor) or `market_scan` (multiple competitors).
   - **Our company**: your company name (required for head-to-head).
   - **Competitors**: the company names you want to research (1–10).
   - **Competitor websites** *(optional but recommended)*: a name → URL map. Skips discovery and goes straight to that vendor's site, which gives much cleaner extraction.
3. Click **Save & Start**. A run typically finishes in 30–90 seconds.
4. Open the run's **Output** tab to see the battlecard, or the **Storage → Default dataset** tab for the structured JSON.

#### From an MCP-enabled LLM (Claude Desktop, Cursor, etc.)

If your MCP client is configured with the Apify MCP server, this Actor is exposed automatically as a tool. The Actor's input schema becomes the tool's argument schema, and the canonical JSON output becomes the tool's result. Just ask: *"Generate a battlecard comparing Linear and Asana"* and the LLM will call this Actor with the right arguments.

#### Programmatically (Apify API)

```bash
curl -X POST "https://api.apify.com/v2/acts/<ACTOR_ID>/runs?token=<TOKEN>" \
  -H "Content-Type: application/json" \
  -d @sample_input.json
````

***

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `mode` | enum | `head_to_head` | `head_to_head` (you vs one competitor) or `market_scan` (multiple competitors). |
| `our_company` | string | — | Your company name. **Required** for `head_to_head`. |
| `our_website` | string | — | Optional canonical URL for your company. Skips discovery. |
| `competitors` | array of string | `[]` | Competitor names. Min 1, max 10. For `head_to_head`, only the first is used. |
| `competitor_websites` | object | — | Optional `{name: url}` map. Skips discovery for those competitors. |
| `confidence_threshold` | int | `60` | If overall confidence falls below this (0–100), the Actor returns ranked candidates instead of guessing. |
| `max_pages_per_company` | int | `6` | Soft cap on HTTP fetches per company. Bumps cost/runtime. |
| `currency_hint` | string | `USD` | Helps pricing normalization when a page exposes multiple currencies. |
| `locale_hint` | string | `en-US` | Helps pick the right localized variant of a pricing page. |
| `user_agent` | string | (polite default) | Override the default identifying User-Agent. |
| `request_timeout_seconds` | int | `20` | Per-request HTTP timeout. |

#### Example input — head-to-head

```json
{
  "mode": "head_to_head",
  "our_company": "Linear",
  "our_website": "https://linear.app",
  "competitors": ["Asana"],
  "competitor_websites": { "Asana": "https://asana.com" },
  "confidence_threshold": 60
}
```

#### Example input — market scan

```json
{
  "mode": "market_scan",
  "competitors": ["Asana", "Notion", "Monday"],
  "competitor_websites": {
    "Asana": "https://asana.com",
    "Notion": "https://www.notion.so",
    "Monday": "https://monday.com"
  },
  "confidence_threshold": 60
}
```

***

### Output

The 14 top-level fields:

| Field | Description |
|---|---|
| `request_context` | Echo of the request: mode, companies, threshold, timestamps. |
| `companies` | Per-company resolution: name, resolved\_website, confidence, candidates, discovered pages. |
| `confidence` | Per-company website/pricing/feature scores plus a composite, and overall vs threshold. |
| `sources` | Flat list of every URL that was fetched, with category (`pricing`/`features`/`homepage`). |
| `pricing_summary` | Per-company plan tiers, prices, billing periods, free/custom flags, lowest paid price. |
| `feature_comparison` | Normalized matrix of features × companies with `yes`/`unknown` coverage flags. |
| `positioning_summary` | Short narrative positioning per company. **Inferred**, clearly labeled. |
| `strengths_and_weaknesses` | Per-competitor strengths and weaknesses with citations. |
| `objection_handling` | Common objections and suggested responses. |
| `win_loss_guidance` | When to lean in, when to walk away. |
| `risk_flags` | Data quality warnings: low confidence, no pricing page, custom-only pricing, etc. |
| `battlecard_markdown` | Readable markdown summary derived from the JSON. |
| `one_pager_text` | Presentation-style brief derived from the JSON. |
| `output_status` | `ok` / `partial` / `candidates_only` / `failed` plus a per-company breakdown. |

#### Trust contract — what this Actor will and will not do

- **Will** cite the source URL for every extracted fact.
- **Will** clearly distinguish extracted facts from inferred insights.
- **Will** return ranked candidates instead of a fabricated answer when discovery confidence is below threshold (`output_status.code = "candidates_only"`).
- **Will** preserve enterprise/contact-sales pricing as `is_custom: true` with the raw text.
- **Will not** invent a price for a vendor whose pricing page doesn't publish one.
- **Will not** spoof user agents or bypass `robots.txt`.

***

### Pricing

This Actor uses **Pay per Result** — $1.00 USD per chargeable competitor analysis.

- **Head-to-head run:** **$1.** Always exactly one billable competitor.
- **Market-scan with N resolved competitors:** **$N.** A 3-competitor scan costs $3, a 5-competitor scan costs $5. Each chargeable competitor gets its own dataset record so you can audit billing per-row.
- **`candidates_only` competitors:** **free.** If the Actor couldn't confirm an official website for a competitor, that competitor isn't billed. (You'll see the ranked candidates in `OUTPUT` so you can retry with the right URL in `competitor_websites`.)
- **`failed` runs:** **free.** Input validation errors and unexpected pipeline failures don't push a record.

**How billing maps to records.** Each chargeable competitor analysis = one dataset record = $1. Open any run → *Storage → Default dataset* → count the records → that's the dollar cost.

**Worked examples:**

| Input | Resolved | Records | Cost |
|---|---|---|---|
| `head_to_head` Linear vs Asana | Asana ✓ | 1 | $1 |
| `head_to_head` Linear vs UnknownCo | UnknownCo ✗ | 0 | $0 |
| `market_scan` \[Asana, Notion, Monday] | All 3 ✓ | 3 | $3 |
| `market_scan` \[Asana, Notion, FakeCo] | 2 of 3 ✓ | 2 | $2 |
| `market_scan` \[FakeCo1, FakeCo2] | 0 of 2 ✓ | 0 | $0 |

The `chargeable_event_count` field in `OUTPUT` (and in every dataset record) tells you the per-run total explicitly.

***

### How it works (short version)

1. **Discovery.** For each company, resolve the canonical website. If you provide it via `competitor_websites`, the Actor uses it directly. Otherwise it tries deterministic TLD guesses (`.com`, `.io`, `.ai`, `.app`, …) and scores each candidate against the company name's appearance in `<title>`, `og:site_name`, and meta description.
2. **Page discovery.** From the homepage, find links matching `/pricing`, `/plans`, `/features`, `/product`, etc. Verify each candidate URL exists.
3. **Extraction.** Rule-based HTML parsing identifies plan-card-shaped DOM regions, extracts plan name, price token, billing period, free/custom flags, and feature bullets. Feature labels are mined from `<h2>`/`<h3>` headings and `<li>` bullets on the features page, then normalized through a tagline/footer/imperative-slogan filter.
4. **Assembly.** The canonical JSON is built first, then `battlecard_markdown` and `one_pager_text` are derived from it. Markdown and one-pager are NEVER the source of truth — the JSON is.

The Actor is **deterministic and explainable**. No LLM API is called at runtime. Same inputs → same outputs.

***

### FAQ

**Q. Will this work for any SaaS company?**
A. It works best for SaaS products with a first-party domain (e.g. `linear.app`, `notion.so`) and a publicly-published pricing page. It struggles with: (a) sub-products under bigger brands like `aws.amazon.com/q/` or `github.com/features/copilot` — pass these via `competitor_websites` and pricing will usually still work; (b) pricing pages rendered entirely client-side in JavaScript — these will return empty plans with a `no_pricing_page` risk flag.

**Q. Why does my run say `candidates_only`?**
A. Because the Actor couldn't confirm an official website for at least one company you named with high enough confidence. Look at `companies[*].candidates` for the ranked alternatives, pick the right one, and re-run with that URL in `competitor_websites`. This is the trust contract working as designed — better an honest "I'm not sure" than a fabricated battlecard.

**Q. Is the output current?**
A. The Actor extracts what's on the vendor's pages right now, at run time. It doesn't cache. If a vendor changed their pricing yesterday, your battlecard reflects yesterday's change. But it's snapshot-only — the Actor doesn't track changes over time (yet).

**Q. Can I trust the strengths/weaknesses analysis?**
A. Treat it as a starting draft, not a finished position. The strengths/weaknesses are derived deterministically from extracted facts (e.g. "no free tier", "lower entry price than ours"), but they don't capture qualitative things like product polish, customer experience, or roadmap. They're meant to seed your prep, not finish it.

**Q. Does this use my data or my prospects' data?**
A. No. The Actor only fetches public marketing pages of the companies you name. It doesn't accept, process, or transmit any personally identifiable information. All output stays in your Apify account's storage.

**Q. Is this legal? Does it violate vendor ToS?**
A. The Actor only fetches publicly available marketing pages, honors `robots.txt`, uses an identifying User-Agent (no spoofing), and rate-limits per host. That's the same posture used by Google, Bing, G2, Capterra, and every other comparison/review site. It's your responsibility to comply with the terms of service of any specific target site.

**Q. Can I run this through an LLM agent?**
A. Yes — that's a first-class use case. The Actor is registered with [Apify MCP](https://docs.apify.com/platform/integrations/mcp), so any MCP-aware client (Claude Desktop, Cursor, ChatGPT with MCP, …) can call it as a tool. Pass natural-language input, get back the structured JSON.

***

### Limitations

- **JS-only pricing pages.** Pricing rendered entirely client-side will yield empty `plans` and a `no_pricing_page` risk flag. v1 is HTML-only by design. An optional LLM-enrichment pass is on the v2 roadmap.
- **Sub-products of big brands.** Companies like AWS, Google Cloud, GitHub features, etc. need an explicit `competitor_websites` URL to work well — discovery on the parent domain is too noisy.
- **Marketing prose vs feature labels.** Some vendors put taglines and slogans in `<h2>` headings on their features page. The normalizer filters most of these (taglines with conjugated verbs, imperative slogans, footer/legal terms) but precision is conservative — a few legitimate features may be filtered out.
- **No historical / change tracking.** Each run is a snapshot. Persistent change detection is on the v2 roadmap.

***

### Changelog

#### 0.2 (launch-prep)

- Added in-output `disclaimer` field — travels with every battlecard.
- Added `chargeable_event_count` field for transparent billing.
- Added robots.txt enforcement and per-host rate limiting.
- Added top-level exception handler — every run produces a structured record.
- Capped `competitors` to 1–10 to bound runtime/cost.
- Locked dependency tree (`requirements.lock.txt`).
- Tightened pricing extraction (no bullet bleed-over between plan cards, free/paid disambiguation).
- Tightened feature extraction (drops footer/legal terms, taglines, imperative slogans).

#### 0.1

- Initial release.

***

### For developers

The remaining sections are for engineers who want to read or modify the code. If you're using the Actor through the console or MCP, you can stop here.

#### Architecture

```
.
├── .actor/
│   ├── actor.json
│   └── input_schema.json
├── OUTPUT_SHAPE.json             ## Reference JSON Schema for the canonical output
├── Dockerfile
├── requirements.txt              ## Loose ranges (intent)
├── requirements.lock.txt         ## Exact pins (production builds)
├── requirements-dev.txt          ## Dev/test extras (pytest)
├── main.py                       ## Apify Actor entrypoint (async)
├── sample_input.json
├── examples/
│   └── copilot_vs_claude_vs_q.json
├── README.md
├── TERMS.md
└── src/
    ├── __init__.py
    ├── models.py                 ## Pydantic models incl. final BattlecardOutput
    ├── utils.py                  ## HTTP, URL, money/period parsing helpers
    ├── robots.py                 ## robots.txt cache + per-host policy
    ├── discovery.py              ## Phase 2: website + pricing/features pages
    ├── extract_pricing.py        ## Phase 3a: rule-based pricing extraction
    ├── extract_features.py       ## Phase 3b: rule-based feature extraction
    └── battlecard.py             ## Phase 4: assemble canonical JSON + MD + 1-pager
```

Each extraction module is **isolated** and testable on its own — they only take HTTP results + already-resolved URLs as inputs. There is no hard dependence on any LLM API. To enable model-assisted enrichment later you would add new functions inside `extract_pricing.py` / `extract_features.py` (or a new `enrichment.py`) and call them after the rule-based pass — the canonical JSON shape doesn't change.

#### Confidence model

For each company we compute three sub-scores (0–100):

- **website** — derived during discovery from domain match, `<title>`, `og:site_name`, and meta description.
- **pricing** — set by `extract_pricing` based on whether structured plans were detected with prices and billing periods.
- **features** — set by `extract_features` based on the count and quality of normalized feature phrases.

The composite is `0.45 × website + 0.35 × pricing + 0.20 × features`. The overall run confidence is the mean across companies. If overall is below `confidence_threshold`, the Actor still emits the JSON but flips `output_status.code` to `candidates_only` or `partial` and adds a `low_confidence_resolution` risk flag.

#### Run locally

```bash
pip install -r requirements.lock.txt
APIFY_LOCAL_STORAGE_DIR=./storage python main.py
```

To use a custom input locally, drop your input JSON into `./storage/key_value_stores/default/INPUT.json` (Apify's local layout).

#### Tests

```bash
pip install -r requirements-dev.txt
pytest
```

107 unit tests, no network, ~150 ms total. Coverage:

- `test_models.py` — Pydantic input validation: empty competitors, head-to-head requires `our_company`, dedupe, threshold/timeout bounds, the 14-field output shape, competitor cap (1–10), disclaimer field.
- `test_utils.py` — money/billing parsing, URL normalization (case-insensitive scheme), domain extraction, `looks_like_free` precision, `looks_like_custom_pricing`.
- `test_extract_pricing.py` — four-tier Linear-shaped page, leaf-card detection (no bullet bleed-over), Free/Basic disambiguation, Enterprise contact-sales detection, fallback heading scan.
- `test_extract_features.py` — heading + bullet mining, blocklist (Legal/Privacy/Press), tagline filter ("X is Y"), imperative-slogan filter, hyphenated terms preserved, features page preferred over homepage.
- `test_battlecard.py` — all top-level fields present, head-to-head goes to `candidates_only` when one side fails, market-scan partial vs candidates-only, derived fields (lowest paid price, feature totals, risk flags), markdown + one-pager rendering, disclaimer appears in markdown.
- `test_robots.py` — robots.txt parsing + caching, allow/deny logic, fail-open on transient errors, per-host rate-limiting.
- `test_smoke.py` — end-to-end: synthetic homepage / pricing / features pages → extractors → assembly → JSON-serializable battlecard with all 14 fields and `output_status.code == "ok"`.

#### Extending

- **Better discovery.** Enable the DuckDuckGo HTML fallback in `discovery.py` (`_ddg_search_top_hits`) when domain-guessing fails.
- **Locale-aware pricing.** Add per-locale pricing-page heuristics to `discovery._conventional_path_guesses`.
- **LLM enrichment.** Add an enrichment pass after `_extract_for_one` in `extract_pricing.py` / `extract_features.py` that takes the rule-based result + the raw HTML and refines it. Keep the canonical JSON shape.
- **Change detection.** Persist past runs in a key-value store and compare pricing snapshots over time — naturally extends `risk_flags` with a `pricing_changed` code.

***

### Terms of Use (summary)

By running this Actor or consuming its output, you agree to the following.

**AS IS, no warranty.** The Actor and its output are provided AS IS and AS AVAILABLE. The operator does not warrant accuracy, currency, completeness, fitness for any purpose, or non-infringement. The Actor's `output_status`, `risk_flags`, and confidence scores indicate the system's own honest uncertainty — treat them as such.

**Limitation of liability.** To the maximum extent permitted by applicable law, the operator's aggregate liability arising out of or related to the Actor or its output shall not exceed the greater of the amount you paid for the Actor in the twelve months preceding the event giving rise to the claim, or USD 100. The operator shall not be liable for indirect, incidental, consequential, special, exemplary, or punitive damages.

**Vendor names.** Vendor names appearing in inputs and outputs are used nominatively to identify the products being researched. No affiliation, endorsement, or sponsorship by any named vendor is claimed or implied. The Actor does not reproduce vendor logos.

**Your responsibilities.** You are solely responsible for compliance with the terms of service of any target site you ask the Actor to fetch, for verifying every extracted fact against the cited source URL before using it in customer-facing material, and for any commercial outcome resulting from your use of the output.

**Prohibited uses.** Do not use the Actor to generate misleading, defamatory, or factually false statements about any vendor; access content behind authentication or paywalls; or process personally identifiable information.

**Data handling.** The Actor processes only publicly available marketing pages. It does not require, accept, or process personally identifiable information. All output is stored exclusively in your Apify account's run dataset and key-value store. The operator does not maintain a separate copy.

For questions, feature requests, or to report issues, contact the Actor maintainer through the Apify console.

# Actor input Schema

## `mode` (type: `string`):

head\_to\_head: our product vs one competitor. market\_scan: multiple competitors without our-product context.

## `our_company` (type: `string`):

Name of your own company. Required when mode = head\_to\_head.

## `our_website` (type: `string`):

If known, the canonical website for your own company. Skips discovery.

## `competitors` (type: `array`):

List of competitor company names. For head\_to\_head, exactly one is used. For market\_scan, multiple. Capped at 10 to keep runtime and cost predictable.

## `competitor_websites` (type: `object`):

Optional map of {competitor\_name: canonical\_website} to skip discovery.

## `confidence_threshold` (type: `integer`):

Auto-proceed when discovery confidence (0-100) is at or above this threshold. Otherwise return ranked candidates.

## `max_pages_per_company` (type: `integer`):

Soft cap on HTTP fetches per company to control runtime and cost.

## `currency_hint` (type: `string`):

Hint for pricing normalization (e.g. USD, EUR). Falls back to whatever the page exposes.

## `locale_hint` (type: `string`):

Hint for picking locale-specific pricing pages (e.g. en-US).

## `user_agent` (type: `string`):

Override the default User-Agent. Use a polite, identifiable UA when scraping.

## `request_timeout_seconds` (type: `integer`):

How long to wait on each HTTP request before giving up. Smaller values are faster but more failure-prone on slow marketing sites.

## Actor input object example

```json
{
  "mode": "head_to_head",
  "our_company": "Linear",
  "our_website": "https://linear.app",
  "competitors": [
    "Asana"
  ],
  "competitor_websites": {
    "Asana": "https://asana.com"
  },
  "confidence_threshold": 60,
  "max_pages_per_company": 6,
  "currency_hint": "USD",
  "locale_hint": "en-US",
  "request_timeout_seconds": 20
}
```

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "our_company": "Linear",
    "our_website": "https://linear.app",
    "competitors": [
        "Asana"
    ],
    "competitor_websites": {
        "Asana": "https://asana.com"
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("entranced_gelato/saas-competitor-battlecards").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "our_company": "Linear",
    "our_website": "https://linear.app",
    "competitors": ["Asana"],
    "competitor_websites": { "Asana": "https://asana.com" },
}

# Run the Actor and wait for it to finish
run = client.actor("entranced_gelato/saas-competitor-battlecards").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "our_company": "Linear",
  "our_website": "https://linear.app",
  "competitors": [
    "Asana"
  ],
  "competitor_websites": {
    "Asana": "https://asana.com"
  }
}' |
apify call entranced_gelato/saas-competitor-battlecards --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=entranced_gelato/saas-competitor-battlecards",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "SaaS Competitor Battlecards",
        "description": "Generate sales-ready competitor battlecards from public pricing & feature pages. Cited sources, deterministic, MCP-ready. $1 per battlecard.",
        "version": "0.2",
        "x-build-id": "T0iGRrFO3HAwT0aTu"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/entranced_gelato~saas-competitor-battlecards/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-entranced_gelato-saas-competitor-battlecards",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/entranced_gelato~saas-competitor-battlecards/runs": {
            "post": {
                "operationId": "runs-sync-entranced_gelato-saas-competitor-battlecards",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/entranced_gelato~saas-competitor-battlecards/run-sync": {
            "post": {
                "operationId": "run-sync-entranced_gelato-saas-competitor-battlecards",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "mode",
                    "competitors"
                ],
                "properties": {
                    "mode": {
                        "title": "Mode",
                        "enum": [
                            "head_to_head",
                            "market_scan"
                        ],
                        "type": "string",
                        "description": "head_to_head: our product vs one competitor. market_scan: multiple competitors without our-product context.",
                        "default": "head_to_head"
                    },
                    "our_company": {
                        "title": "Our company",
                        "type": "string",
                        "description": "Name of your own company. Required when mode = head_to_head.",
                        "default": "Linear"
                    },
                    "our_website": {
                        "title": "Our company website (optional)",
                        "type": "string",
                        "description": "If known, the canonical website for your own company. Skips discovery.",
                        "default": "https://linear.app"
                    },
                    "competitors": {
                        "title": "Competitors",
                        "minItems": 1,
                        "maxItems": 10,
                        "type": "array",
                        "description": "List of competitor company names. For head_to_head, exactly one is used. For market_scan, multiple. Capped at 10 to keep runtime and cost predictable.",
                        "items": {
                            "type": "string"
                        },
                        "default": [
                            "Asana"
                        ]
                    },
                    "competitor_websites": {
                        "title": "Competitor websites (optional)",
                        "type": "object",
                        "description": "Optional map of {competitor_name: canonical_website} to skip discovery."
                    },
                    "confidence_threshold": {
                        "title": "Confidence threshold",
                        "minimum": 0,
                        "maximum": 100,
                        "type": "integer",
                        "description": "Auto-proceed when discovery confidence (0-100) is at or above this threshold. Otherwise return ranked candidates.",
                        "default": 60
                    },
                    "max_pages_per_company": {
                        "title": "Max pages fetched per company",
                        "minimum": 1,
                        "maximum": 30,
                        "type": "integer",
                        "description": "Soft cap on HTTP fetches per company to control runtime and cost.",
                        "default": 6
                    },
                    "currency_hint": {
                        "title": "Preferred currency (optional)",
                        "type": "string",
                        "description": "Hint for pricing normalization (e.g. USD, EUR). Falls back to whatever the page exposes.",
                        "default": "USD"
                    },
                    "locale_hint": {
                        "title": "Preferred locale (optional)",
                        "type": "string",
                        "description": "Hint for picking locale-specific pricing pages (e.g. en-US).",
                        "default": "en-US"
                    },
                    "user_agent": {
                        "title": "Custom User-Agent (optional)",
                        "type": "string",
                        "description": "Override the default User-Agent. Use a polite, identifiable UA when scraping."
                    },
                    "request_timeout_seconds": {
                        "title": "Per-request timeout (seconds)",
                        "minimum": 5,
                        "maximum": 120,
                        "type": "integer",
                        "description": "How long to wait on each HTTP request before giving up. Smaller values are faster but more failure-prone on slow marketing sites.",
                        "default": 20
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
