# Shopify Multi-Store Vendor & Type Comparison (`practical_ai_lab_jp/pal-shopify-multi-store-vendor-compare`) Actor

Compare vendors and product-type coverage across permitted storefront public feeds. For Shopify competitive assortment planners. Check the live Pricing tab and input limits.

- **URL**: https://apify.com/practical_ai_lab_jp/pal-shopify-multi-store-vendor-compare.md
- **Developed by:** [Practical AI Lab](https://apify.com/practical_ai_lab_jp) (community)
- **Categories:** E-commerce, Developer tools
- **Stats:** 2 total users, 1 monthly users, 50.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 comparison rows

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/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Shopify Multi-Store Vendor & Type Comparison

Compare the **public product feeds of two or three Shopify stores** by published **vendor**, **product type**, or **vendor + type**. Every result comes with explicit disclosures about how much of each catalog was actually sampled, so you can judge how far to trust the numbers.

### What it does

For each store the Actor checks `robots.txt`, then reads the public `/products.json` feed in pages of 250 products, up to your per-store cap. It then groups the sampled products and outputs one row per group showing, for every store:

- number of sampled products and share of that store's sample
- median, lowest and highest "lowest variant price" per product
- share of products with at least one available variant
- a `lowSample` flag when a store has fewer than 5 products in the group

Each group row also reports how many stores carry the group, which store leads by sampled product count (empty on ties), and the median price gap between stores.

A separate `store_coverage` row per store reports products sampled, pages read, whether the sample was truncated, distinct vendor/type counts, and the share of products with blank vendor or type.

### Example input

```json
{
    "stores": ["store-one.example.com", "store-two.example.com"],
    "groupBy": "vendor",
    "maxProductsPerStore": 500,
    "minGroupProducts": 3,
    "maxGroups": 50,
    "requestDelaySecs": 1
}
```

Replace the placeholder domains with real Shopify store domains you are allowed to analyze.

### Example output (shape)

```json
{
    "recordType": "group_comparison",
    "groupBy": "vendor",
    "groupName": "Acme",
    "totalProductsInSamples": 42,
    "storesWithGroup": 2,
    "presentInAllStores": true,
    "leadingStore": "store-one.example.com",
    "medianPriceGapPct": 18.4,
    "perStore": [
        {"store": "store-one.example.com", "products": 30, "shareOfSamplePct": 6.0,
         "medianLowestVariantPrice": 24.0, "lowestPrice": 9.0, "highestPrice": 80.0,
         "availableSharePct": 86.7, "lowSample": false}
    ],
    "samplingNote": "Counts describe only the sampled feed products ..."
}
```

(Values above illustrate the structure only; they are not real results.)

### Limitations and sample-size caveats

- **Sample, not census.** Only the first products in feed order are read, up to your cap (max 2000 per store). Feed order is not random, so shares can be biased when a catalog is larger than the cap. Truncated stores are flagged `possiblyTruncated: true`.
- **Different sample sizes per store** make "leading store" comparisons about the sample, not about real catalog size.
- **Small groups are noisy.** Medians from fewer than 5 products are flagged `lowSample`; treat them as anecdotal.
- **Currency is not in the feed.** Prices are shown in each store's own unlabeled currency and are not converted. The price gap is only meaningful if the stores sell in the same currency.
- **Published vendor/type labels are merchant-entered.** Spelling variants beyond letter case and spacing are treated as different groups, and blank values appear as `(not published)`.
- Hidden, unpublished or password-protected products are not in the public feed. Some stores disable the feed, block it in `robots.txt`, or redirect to another host; these are reported as errors and skipped.
- The Actor does not measure sales, traffic, or demand. Product counts are not revenue.
- A page shorter than 250 products is treated as the end of the feed; a store that silently serves smaller pages could be reported as complete when it is not.

### Safety and compliance

- Only `https` store domains are accepted. IP addresses, credentials, custom ports, local/internal hostnames, and domains resolving to non-public addresses are refused.
- Redirects are followed only within the same host.
- `robots.txt` is honored and fails closed if it cannot be verified. `Crawl-delay` is respected (capped at 10 seconds).
- Requests are sequential, delayed, size-limited and time-limited. No proxies, logins, or personal data are used. The feed's vendor and product fields describe products, not people.
- You are responsible for ensuring your use of the data complies with each store's terms and applicable law.

### Billing

The code emits a pay-per-event style event named `comparison-row`, once per output group row, after the rows are stored. Store coverage rows and failed runs are never charged. No payable plan or price is configured in this repository; pricing must be set separately in the platform after review.

### Local development

Tests are fully offline (no network, no Apify platform required for the test suite):

```
python -m unittest discover -s tests -t .
```

Project layout: `src/validation.py` (input and URL safety), `src/fetcher.py` (robots and paging), `src/analysis.py` (grouping and statistics), `src/main.py` (Actor entry point).

# Actor input Schema

## `stores` (type: `array`):

Two or three Shopify store domains or https URLs, for example allbirds.com. Only the host is used; paths, ports and credentials are rejected.

## `groupBy` (type: `string`):

Which published field defines a comparison group.

## `maxProductsPerStore` (type: `integer`):

Upper bound on products read from each feed (250 per request). If the catalog is larger, the sample is the first products in feed order and the store is flagged as truncated.

## `minGroupProducts` (type: `integer`):

Hide groups whose combined sampled product count across all stores is below this number.

## `maxGroups` (type: `integer`):

Only the largest groups (by sampled products) are output. Each output group row is one billable result.

## `requestDelaySecs` (type: `number`):

Pause between feed page requests to the same store. robots.txt Crawl-delay is honored if larger (capped at 10 seconds).

## Actor input object example

```json
{
  "stores": [
    "store-one.example.com",
    "store-two.example.com"
  ],
  "groupBy": "vendor",
  "maxProductsPerStore": 50,
  "minGroupProducts": 1,
  "maxGroups": 50,
  "requestDelaySecs": 1
}
```

# Actor output Schema

## `results` (type: `string`):

Download rows from the latest run in JSON/CSV.

# 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 = {
    "stores": [
        "www.allbirds.com",
        "www.tentree.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("practical_ai_lab_jp/pal-shopify-multi-store-vendor-compare").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 = { "stores": [
        "www.allbirds.com",
        "www.tentree.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("practical_ai_lab_jp/pal-shopify-multi-store-vendor-compare").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "stores": [
    "www.allbirds.com",
    "www.tentree.com"
  ]
}' |
apify call practical_ai_lab_jp/pal-shopify-multi-store-vendor-compare --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,practical_ai_lab_jp/pal-shopify-multi-store-vendor-compare"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/8OEI5P8gofhhQgrZL/builds/0HR7ShAPN9mf0RReM/openapi.json
