# Supplier Price-Change Bot (`civic-data-tools/my-actor`) Actor

Compare two supplier CSV or JSON price lists. Detect price increases, smaller packs at the same price, and changes in unit costs. Exact SKU matching, reviewable exceptions, and HTML/CSV/JSON reports. No AI key or supplier login needed.

- **URL**: https://apify.com/civic-data-tools/my-actor.md
- **Developed by:** [Civic Data Tools](https://apify.com/civic-data-tools) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.10 / completed price-list comparison

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

## Supplier Price-Change Bot

Compare two supplier price lists and identify price increases, pack-size changes, and changes in cost per gram, millilitre, or item. Supply CSV text or structured rows. Download a readable HTML report, changes CSV, exceptions CSV, and full JSON.

Useful for purchasing teams, small retailers, restaurant operators, and workflows that receive recurring supplier exports. No calls, onboarding meeting, supplier login, or AI model key is needed.

### Quick start

Run the prefilled fictional example, then replace both lists with your own. For each snapshot provide exactly one of `previousRows`/`previousCsv` and `currentRows`/`currentCsv`.

```json
{
  "previousRows": [{"sku":"001","price":"12.00","currency":"USD","pack":"12 x 330 ml"}],
  "currentRows": [{"sku":"001","price":"12.00","currency":"USD","pack":"12 x 300 ml"}]
}
```

This example detects an unchanged pack price and a 10% increase in price per millilitre. Numeric amounts are calculated with decimal arithmetic. Percentages from an old price of zero are undefined and returned as null.

### CSV and matching

Use columns such as `sku,price,currency,pack`. Column maps translate your exact header names, for example `previousColumnMap: {"sku":"Item Number","price":"Net Cost"}`. Explicit maps are required if several headers could represent the same field. Specify `decimalSeparator` and `delimiter` explicitly for decimal-comma files. Prices must be nonnegative plain numbers without currency symbols; supply a three-letter currency or `defaultCurrency`.

Products match by supplier, SKU, and optional variant. Leading zeroes and SKU case are preserved; `caseSensitiveSku: false` is an explicit option. Duplicate identities and invalid records are reported as exceptions. Missing product identifiers prevent reliable addition/removal conclusions. A product removed from a list is not necessarily discontinued or unavailable.

### Packs and limitations

Supported examples include `500 g`, `0.5 kg`, `12 x 330 ml`, and `24 each`. Structured `pack_count`, `unit_size`, and `unit` fields are also supported. Explicit `us_fl_oz` is available; ambiguous labels such as `case`, `bottle`, and `fl oz` are withheld from unit-cost comparisons. Mass and volume are never compared against each other. Currency changes are flagged without exchange-rate conversion.

Maximum per snapshot: 5,000 rows, 4 MiB of CSV, 60 columns, and 2,000 characters per cell. Empty snapshots are rejected. This version does not extract PDFs, perform OCR, fetch URLs, scrape supplier websites, infer fuzzy product matches, or save a customer's baseline between separate runs. Customers supply both snapshots and may invoke the Actor from their own recurring workflow.

### Outputs

The default key-value store contains `OUTPUT` (JSON), `REPORT` (HTML), `CHANGES` (CSV), and `EXCEPTIONS` (CSV). The dataset contains one comparison report, including summary, changes, exceptions, and a comparison fingerprint. Review exceptions before acting on results. HTML text is escaped; spreadsheet-formula prefixes are escaped in CSV exports.

### Pricing and billing

Consult the live Store pricing panel for the actual price. The intended billable event is `comparison-completed`: one event for a completed valid comparison, including comparisons with no changes. It is not charged per row or per change. Invalid inputs are rejected before this custom charge. Separate new runs are separate comparisons and may be billed again.

Charging is disabled by default in this source package. When enabled, the runtime requires pay-per-event pricing and checks the available event budget. Same-run recovery records prevent ordinary duplicate charges. An uncertain charge outcome stops for reconciliation rather than automatically charging again. Local test charges are simulations and are not revenue. Platform usage charges, if applicable, follow Apify's live pricing.

### Local verification

Python 3.11+ is required; the cloud image uses Python 3.13.

```sh
python3 -m unittest discover -s tests -v
python3 -m supplier_bot.cli --input samples/demo-input.json --output reports/demo
pip install -r requirements.txt
python scripts/verify_sdk.py --storage /tmp/supplier-price-bot-test
```

No external API calls are needed by the comparison engine. Uploaded inputs and outputs are stored in the user's Apify run storage and follow that account's retention and access settings. Never publish confidential supplier data as a public sample. All bundled examples are fictional.

# Actor input Schema

## `previousRows` (type: `array`):

Previous snapshot as objects. Use either this or Previous CSV, never both.

## `currentRows` (type: `array`):

Current snapshot as objects. Use either this or Current CSV, never both.

## `previousCsv` (type: `string`):

Inline CSV text with SKU and price columns. Remove Previous rows when using this.

## `currentCsv` (type: `string`):

Inline CSV text with SKU and price columns. Remove Current rows when using this.

## `defaultCurrency` (type: `string`):

Explicit three-letter currency code when not supplied on each row.

## `supplier` (type: `string`):

Optional supplier identity applied to rows lacking a supplier column.

## `previousColumnMap` (type: `object`):

Canonical field to exact CSV header, e.g. {"sku":"Item Code","price":"Cost"}. See README for supported fields.

## `currentColumnMap` (type: `object`):

Canonical field to exact current header. Supply explicit mappings when aliases collide.

## `decimalSeparator` (type: `string`):

Numeric decimal convention for both snapshots.

## `delimiter` (type: `string`):

Delimiter used by both CSV snapshots.

## `caseSensitiveSku` (type: `boolean`):

Disable only if the supplier explicitly treats letter case as irrelevant.

## Actor input object example

```json
{
  "previousRows": [
    {
      "sku": "001",
      "description": "Fictional sparkling water",
      "price": "12.00",
      "currency": "USD",
      "pack": "12 x 330 ml"
    },
    {
      "sku": "002",
      "description": "Fictional coffee",
      "price": "10.00",
      "currency": "USD",
      "pack": "500 g"
    },
    {
      "sku": "003",
      "description": "Fictional napkins",
      "price": "5.00",
      "currency": "USD",
      "pack": "100 each"
    },
    {
      "sku": "004",
      "description": "Fictional obsolete list item",
      "price": "8.00",
      "currency": "USD",
      "pack": "4 each"
    },
    {
      "sku": "005",
      "description": "Fictional unchanged soap",
      "price": "3.00",
      "currency": "USD",
      "pack": "1 each"
    },
    {
      "sku": "006",
      "description": "Fictional ambiguous case",
      "price": "6.00",
      "currency": "USD",
      "pack": "case"
    },
    {
      "sku": "007",
      "description": "Fictional duplicate red",
      "price": "2.00",
      "currency": "USD",
      "pack": "1 each"
    },
    {
      "sku": "007",
      "description": "Fictional duplicate blue",
      "price": "3.00",
      "currency": "USD",
      "pack": "1 each"
    },
    {
      "sku": "008",
      "description": "Fictional currency switch",
      "price": "10.00",
      "currency": "USD",
      "pack": "1 kg"
    }
  ],
  "currentRows": [
    {
      "sku": "001",
      "description": "Fictional sparkling water",
      "price": "12.00",
      "currency": "USD",
      "pack": "12 x 300 ml"
    },
    {
      "sku": "002",
      "description": "Fictional coffee",
      "price": "11.00",
      "currency": "USD",
      "pack": "0.5 kg"
    },
    {
      "sku": "003",
      "description": "Fictional napkins",
      "price": "4.50",
      "currency": "USD",
      "pack": "100 each"
    },
    {
      "sku": "005",
      "description": "Fictional unchanged soap",
      "price": "3.00",
      "currency": "USD",
      "pack": "1 each"
    },
    {
      "sku": "006",
      "description": "Fictional ambiguous case",
      "price": "6.00",
      "currency": "USD",
      "pack": "case"
    },
    {
      "sku": "007",
      "description": "Fictional duplicate red",
      "price": "4.00",
      "currency": "USD",
      "pack": "1 each"
    },
    {
      "sku": "008",
      "description": "Fictional currency switch",
      "price": "10.00",
      "currency": "EUR",
      "pack": "1 kg"
    },
    {
      "sku": "009",
      "description": "Fictional new item",
      "price": "7.00",
      "currency": "USD",
      "pack": "12 each"
    }
  ],
  "decimalSeparator": ".",
  "delimiter": ",",
  "caseSensitiveSku": true
}
```

# Actor output Schema

## `report` (type: `string`):

No description

## `changes` (type: `string`):

No description

## `exceptions` (type: `string`):

No description

## `html` (type: `string`):

No description

# 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 = {
    "previousRows": [
        {
            "sku": "001",
            "description": "Fictional sparkling water",
            "price": "12.00",
            "currency": "USD",
            "pack": "12 x 330 ml"
        },
        {
            "sku": "002",
            "description": "Fictional coffee",
            "price": "10.00",
            "currency": "USD",
            "pack": "500 g"
        },
        {
            "sku": "003",
            "description": "Fictional napkins",
            "price": "5.00",
            "currency": "USD",
            "pack": "100 each"
        },
        {
            "sku": "004",
            "description": "Fictional obsolete list item",
            "price": "8.00",
            "currency": "USD",
            "pack": "4 each"
        },
        {
            "sku": "005",
            "description": "Fictional unchanged soap",
            "price": "3.00",
            "currency": "USD",
            "pack": "1 each"
        },
        {
            "sku": "006",
            "description": "Fictional ambiguous case",
            "price": "6.00",
            "currency": "USD",
            "pack": "case"
        },
        {
            "sku": "007",
            "description": "Fictional duplicate red",
            "price": "2.00",
            "currency": "USD",
            "pack": "1 each"
        },
        {
            "sku": "007",
            "description": "Fictional duplicate blue",
            "price": "3.00",
            "currency": "USD",
            "pack": "1 each"
        },
        {
            "sku": "008",
            "description": "Fictional currency switch",
            "price": "10.00",
            "currency": "USD",
            "pack": "1 kg"
        }
    ],
    "currentRows": [
        {
            "sku": "001",
            "description": "Fictional sparkling water",
            "price": "12.00",
            "currency": "USD",
            "pack": "12 x 300 ml"
        },
        {
            "sku": "002",
            "description": "Fictional coffee",
            "price": "11.00",
            "currency": "USD",
            "pack": "0.5 kg"
        },
        {
            "sku": "003",
            "description": "Fictional napkins",
            "price": "4.50",
            "currency": "USD",
            "pack": "100 each"
        },
        {
            "sku": "005",
            "description": "Fictional unchanged soap",
            "price": "3.00",
            "currency": "USD",
            "pack": "1 each"
        },
        {
            "sku": "006",
            "description": "Fictional ambiguous case",
            "price": "6.00",
            "currency": "USD",
            "pack": "case"
        },
        {
            "sku": "007",
            "description": "Fictional duplicate red",
            "price": "4.00",
            "currency": "USD",
            "pack": "1 each"
        },
        {
            "sku": "008",
            "description": "Fictional currency switch",
            "price": "10.00",
            "currency": "EUR",
            "pack": "1 kg"
        },
        {
            "sku": "009",
            "description": "Fictional new item",
            "price": "7.00",
            "currency": "USD",
            "pack": "12 each"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("civic-data-tools/my-actor").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 = {
    "previousRows": [
        {
            "sku": "001",
            "description": "Fictional sparkling water",
            "price": "12.00",
            "currency": "USD",
            "pack": "12 x 330 ml",
        },
        {
            "sku": "002",
            "description": "Fictional coffee",
            "price": "10.00",
            "currency": "USD",
            "pack": "500 g",
        },
        {
            "sku": "003",
            "description": "Fictional napkins",
            "price": "5.00",
            "currency": "USD",
            "pack": "100 each",
        },
        {
            "sku": "004",
            "description": "Fictional obsolete list item",
            "price": "8.00",
            "currency": "USD",
            "pack": "4 each",
        },
        {
            "sku": "005",
            "description": "Fictional unchanged soap",
            "price": "3.00",
            "currency": "USD",
            "pack": "1 each",
        },
        {
            "sku": "006",
            "description": "Fictional ambiguous case",
            "price": "6.00",
            "currency": "USD",
            "pack": "case",
        },
        {
            "sku": "007",
            "description": "Fictional duplicate red",
            "price": "2.00",
            "currency": "USD",
            "pack": "1 each",
        },
        {
            "sku": "007",
            "description": "Fictional duplicate blue",
            "price": "3.00",
            "currency": "USD",
            "pack": "1 each",
        },
        {
            "sku": "008",
            "description": "Fictional currency switch",
            "price": "10.00",
            "currency": "USD",
            "pack": "1 kg",
        },
    ],
    "currentRows": [
        {
            "sku": "001",
            "description": "Fictional sparkling water",
            "price": "12.00",
            "currency": "USD",
            "pack": "12 x 300 ml",
        },
        {
            "sku": "002",
            "description": "Fictional coffee",
            "price": "11.00",
            "currency": "USD",
            "pack": "0.5 kg",
        },
        {
            "sku": "003",
            "description": "Fictional napkins",
            "price": "4.50",
            "currency": "USD",
            "pack": "100 each",
        },
        {
            "sku": "005",
            "description": "Fictional unchanged soap",
            "price": "3.00",
            "currency": "USD",
            "pack": "1 each",
        },
        {
            "sku": "006",
            "description": "Fictional ambiguous case",
            "price": "6.00",
            "currency": "USD",
            "pack": "case",
        },
        {
            "sku": "007",
            "description": "Fictional duplicate red",
            "price": "4.00",
            "currency": "USD",
            "pack": "1 each",
        },
        {
            "sku": "008",
            "description": "Fictional currency switch",
            "price": "10.00",
            "currency": "EUR",
            "pack": "1 kg",
        },
        {
            "sku": "009",
            "description": "Fictional new item",
            "price": "7.00",
            "currency": "USD",
            "pack": "12 each",
        },
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("civic-data-tools/my-actor").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 '{
  "previousRows": [
    {
      "sku": "001",
      "description": "Fictional sparkling water",
      "price": "12.00",
      "currency": "USD",
      "pack": "12 x 330 ml"
    },
    {
      "sku": "002",
      "description": "Fictional coffee",
      "price": "10.00",
      "currency": "USD",
      "pack": "500 g"
    },
    {
      "sku": "003",
      "description": "Fictional napkins",
      "price": "5.00",
      "currency": "USD",
      "pack": "100 each"
    },
    {
      "sku": "004",
      "description": "Fictional obsolete list item",
      "price": "8.00",
      "currency": "USD",
      "pack": "4 each"
    },
    {
      "sku": "005",
      "description": "Fictional unchanged soap",
      "price": "3.00",
      "currency": "USD",
      "pack": "1 each"
    },
    {
      "sku": "006",
      "description": "Fictional ambiguous case",
      "price": "6.00",
      "currency": "USD",
      "pack": "case"
    },
    {
      "sku": "007",
      "description": "Fictional duplicate red",
      "price": "2.00",
      "currency": "USD",
      "pack": "1 each"
    },
    {
      "sku": "007",
      "description": "Fictional duplicate blue",
      "price": "3.00",
      "currency": "USD",
      "pack": "1 each"
    },
    {
      "sku": "008",
      "description": "Fictional currency switch",
      "price": "10.00",
      "currency": "USD",
      "pack": "1 kg"
    }
  ],
  "currentRows": [
    {
      "sku": "001",
      "description": "Fictional sparkling water",
      "price": "12.00",
      "currency": "USD",
      "pack": "12 x 300 ml"
    },
    {
      "sku": "002",
      "description": "Fictional coffee",
      "price": "11.00",
      "currency": "USD",
      "pack": "0.5 kg"
    },
    {
      "sku": "003",
      "description": "Fictional napkins",
      "price": "4.50",
      "currency": "USD",
      "pack": "100 each"
    },
    {
      "sku": "005",
      "description": "Fictional unchanged soap",
      "price": "3.00",
      "currency": "USD",
      "pack": "1 each"
    },
    {
      "sku": "006",
      "description": "Fictional ambiguous case",
      "price": "6.00",
      "currency": "USD",
      "pack": "case"
    },
    {
      "sku": "007",
      "description": "Fictional duplicate red",
      "price": "4.00",
      "currency": "USD",
      "pack": "1 each"
    },
    {
      "sku": "008",
      "description": "Fictional currency switch",
      "price": "10.00",
      "currency": "EUR",
      "pack": "1 kg"
    },
    {
      "sku": "009",
      "description": "Fictional new item",
      "price": "7.00",
      "currency": "USD",
      "pack": "12 each"
    }
  ]
}' |
apify call civic-data-tools/my-actor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,civic-data-tools/my-actor"
        }
    }
}
```

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/XzDBLNA802HOMAR5O/builds/JcRoa0daNik9pmO5V/openapi.json
