# RFP Amendment and Addendum Analyzer (`nickatnite79/rfp-amendment-analyzer`) Actor

Compare RFP, tender and procurement revisions or addenda for deadline and requirement changes. Return JSON changes with source quotations and page citations.

- **URL**: https://apify.com/nickatnite79/rfp-amendment-analyzer.md
- **Developed by:** [Nicholas Tamagni](https://apify.com/nickatnite79) (community)
- **Categories:** Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$250.00 / 1,000 rfp amendment comparisons

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

## RFP Amendment and Addendum Analyzer

Compare an original RFP, tender, or procurement document with amendment documents. The Actor returns statement changes, source quotations, page references, and deterministic suggestions about proposal sections to review.

This is useful for a first pass over revised submission deadlines, mandatory requirements, and other wording changes. It uses text extraction and rules, with no LLM or OCR. Its classifications and proposal-section suggestions are heuristics for human review; they do not establish compliance, legal meaning, or whether a change applies to a particular bid.

### Agent selection and use

Choose this Actor when the user already has an original RFP, tender, or solicitation and asks what wording changed in a revised document or addendum, which deadline or requirement passages need review, or which proposal sections may need attention. It returns page-cited JSON changes suitable for an agent's review workflow.

| Documents supplied | Set `comparisonMode` | Expected JSON changes |
| --- | --- | --- |
| Original plus a complete revised version | `replacement` | Statement additions, removals, and suggested modifications between the two versions. |
| Original plus a separate amendment or supplement | `addendum` | New supplemental statements and suggested similarity matches; omitted original statements are never reported as deleted. |

Supply `original` and 1–50 entries in `amendments`, each with exactly one of native `text` or a direct public PDF/plain-text `url`. Set the mode deliberately: a document labeled “amendment” is not necessarily a complete replacement. Every comparison uses the supplied original as its baseline. Use the input below through Apify's API or MCP with the caller's Apify credentials; no separate LLM or OCR API key is required.

Read `status` first. For successful records, inspect `warnings`, `sources.*.noTextPages`, and `summary.truncatedChanges` before summarizing coverage. Use `changes[].before`, `after`, and `citations` to show the passages that changed; use `category`, `severity`, and `proposalSections` to suggest review priorities. A citation with `relation: "context"` is an opposing source anchor, not a quotation of the added or removed clause. These cues support a decision about what to inspect; they do not approve a proposal or establish the legal effect of an amendment.

For tender discovery, semantic interpretation of “replace clause” instructions, cumulative amendment consolidation, OCR, or a complete compliance audit, select a tool that provides that capability. This Actor compares extracted text, and its modified-statement pairings and deadline/requirement labels are heuristic. Scanned PDFs with no text produce errors; images, table layout, and unsupported formats are not analyzed.

Billing is per successful amendment comparison, including an unchanged or empty native-text comparison with `summary.totalChanges: 0`. One comparison containing many changes still produces one billable result. Error records do not emit the custom `result` event. Consult the live Store pricing and check the caller's spending limit before submitting multiple amendments.

### Quick start

Supply the original and at least one amendment, choosing either `text` or `url` for each document:

```json
{
  "original": {
    "name": "Original RFP",
    "text": "Proposals must be received by 15 October 2026 at 17:00 UTC."
  },
  "amendments": [
    {
      "name": "Amendment 1",
      "text": "Proposals must be received by 22 October 2026 at 17:00 UTC."
    }
  ],
  "comparisonMode": "replacement",
  "maxChanges": 500
}
```

Run the Actor and open its default dataset. You receive one record per amendment with `status: "success"` or `status: "error"`. Each amendment is compared independently against the original; amendments are never applied cumulatively.

### Call from an AI agent

The Actor name is `Nickatnite79/rfp-amendment-analyzer`. Through Apify MCP, fetch its details, supply the Quick start input, run it by name, and retrieve the resulting dataset. Inspect status, extraction warnings and citation relations before summarizing changes.

With `apify-client` installed and the caller's credentials configured as `APIFY_TOKEN`, this example compares one synthetic deadline and retrieves the JSON output:

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('Nickatnite79/rfp-amendment-analyzer').call({
  original: { name: 'Synthetic original', text: 'Proposals must be received by 15 October 2026 at 17:00 UTC.' },
  amendments: [{ name: 'Synthetic revision', text: 'Proposals must be received by 22 October 2026 at 17:00 UTC.' }],
  comparisonMode: 'replacement',
}, { maxTotalChargeUsd: 1 });
if (run.status !== 'SUCCEEDED') throw new Error(`Actor run status: ${run.status}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

The $1 spending cap is for one comparison at the current event price. Read current pricing before larger requests. A successful run can contain comparison error records; inspect every record's `status` and error details.

### Input

| Field | Required | Meaning |
| --- | --- | --- |
| `original` | Yes | Document object with exactly one of `text` or `url`, and optional `name`. |
| `amendments` | Yes | Array of 1 to 50 document objects with the same shape as `original`. |
| `comparisonMode` | No | `replacement` (default) or `addendum`. |
| `maxChanges` | No | Maximum returned changes per amendment, from 1 to 2,000; default 500. |
| `includeBoilerplate` | No | Addendum mode only. Default `false` omits boilerplate (see below); `true` returns every new statement. |

Use **replacement** when the amendment is a complete revised document. The Actor can report statements as `added`, `removed`, or `modified`.

Use **addendum** for a separate supplement. New statements are returned as additions, or suggested modifications when their wording is similar to an original statement. Omission from a supplement is not treated as removal. The Actor does not interpret replacement instructions or apply them to the original.

In addendum mode, boilerplate is omitted by default and counted in `summary.filteredBoilerplate`: form fields (`Company Name:`, `Signature:`, `Date:`), signature/acknowledgment blocks, `***End of Addendum***`, addendum headings and issue dates, page headers/footers repeated across pages, solicitation title lines, standard language such as "all other terms and conditions remain unchanged", and passages that restate original text verbatim. Statements with a changed date, number or obligation word are never filtered as boilerplate. Schedule rows that carry dates are paired by their event label (for example "Submission of Proposals Due"), so a moved date is matched to the same event rather than to a neighbouring row.

#### Document formats

- Native `text` supports plain text and Markdown. A form-feed character, written as `\f` inside JSON strings, marks a page boundary. Empty native documents are valid; two empty or otherwise identical documents yield zero changes.
- A `url` must use HTTP or HTTPS and resolve to a public PDF or plain-text resource. Supply a direct download URL. HTML pages, private network destinations, and authenticated downloads are unsupported.
- PDFs are extracted with PDF.js. Page numbers refer to physical PDF pages, not printed page labels. Scanned PDFs require text extraction or OCR before use: a PDF with no extractable text on any page returns an error. If only some pages have no text, the result includes a warning and those pages cannot contribute changes.

Downloads are limited to 10 MiB per document. Extracted text is limited to 2,000,000 characters, 2,000 pages, and 20,000 statements per document. Comparisons also have a computational bound; very large or substantially different documents can return a comparison error. `maxChanges` limits output size, not the work needed to compare documents.

### Results

Successful records include these fields:

| Field | Meaning |
| --- | --- |
| `status` | `success`; failed records use `error` and include `error.code` and `error.message`. |
| `amendmentIndex` | The amendment's 1-based position in the input array. A global error can use `null`. |
| `sources.original`, `sources.amendment` | Source metadata: `name`, `url`, `type`, `pageCount`, `noTextPages`, and `statementCount`. |
| `comparisonMode` | The selected comparison mode. |
| `summary` | Counts: `totalChanges`, `returnedChanges`, `truncatedChanges`, `added`, `removed`, `modified`, `requirements`, `deadlines`, `other`, and `filteredBoilerplate` (addendum-mode statements omitted as boilerplate). |
| `changes` | Returned changes, each with before/after text, classification, matching method, and source citations. |
| `warnings` | Issues affecting interpretation, such as PDF pages with no extractable text. |
| `limitations` | Qualifications about extraction, matching, and heuristic suggestions. |

Summary counts cover all detected changes even when `maxChanges` truncates the returned list. Category counts (`requirements`, `deadlines`, `other`) and change-type counts (`added`, `removed`, `modified`) describe separate ways of grouping the same changes.

Error records contain `status`, `amendmentIndex`, `comparisonMode`, and `error: {"code": "…", "message": "…"}`. The core marks errors with `_billable: false`; the runtime consumes that internal marker before storing dataset records. A comparison error does not contain a successful change assessment.

Each change contains:

- `id` and `type` (`added`, `removed`, or `modified`). `before` or `after` is `null` when that side has no changed statement.
- `category` (`deadline`, `requirement`, or `other`) and `severity` (`high`, `medium`, or `low`), with a `severityReason`. These are rule-based review cues, not measures of contractual importance.
- `assessmentMethod: "deterministic-rules"`, `suggestionsAreHeuristic: true`, and a `proposalSections` array suggesting sections to inspect.
- `matchMethod`: `exact-sequence`, `token-similarity`, or `addendum-new-statement`. Token similarity can pair related wording as a modification; a substantially rewritten clause may instead appear as a removal and an addition.
- `citations.original` and `citations.amendment`, each identifying the source, name, URL, page, quotation, and relation.

#### Citation meanings

`relation: "changed-text"` identifies the changed statement itself. For an addition or removal, the opposite document's citation uses `relation: "context"` with an explicit note and a nearby source statement as an anchor. That context quotation is not evidence that the added or removed text appears there, and the anchor may itself have changed. If the source has no extracted statements, the counterpart citation uses `relation: "absence"` with `page` and `quote` both `null`; an absence cannot be proved by quoting a page.

Native text pages are numbered from one using supplied form-feed boundaries. Citation quotations preserve internal whitespace from extracted statements; the comparison normalizes whitespace. PDF quotations come from extracted text, so reading order, whitespace, columns, and tables can differ from the visible layout.

#### Output example

The following illustrative successful record shows the shape of a deadline modification. The quotations match the input above. Exact IDs, explanation text, and proposal-section suggestions are generated by the Actor's rules.

```json
{
  "status": "success",
  "amendmentIndex": 1,
  "comparisonMode": "replacement",
  "sources": {
    "original": {
      "name": "Original RFP",
      "url": null,
      "type": "text",
      "pageCount": 1,
      "noTextPages": [],
      "statementCount": 1
    },
    "amendment": {
      "name": "Amendment 1",
      "url": null,
      "type": "text",
      "pageCount": 1,
      "noTextPages": [],
      "statementCount": 1
    }
  },
  "summary": {
    "totalChanges": 1,
    "returnedChanges": 1,
    "truncatedChanges": 0,
    "added": 0,
    "removed": 0,
    "modified": 1,
    "requirements": 0,
    "deadlines": 1,
    "other": 0,
    "filteredBoilerplate": 0
  },
  "changes": [
    {
      "id": "change-1",
      "type": "modified",
      "category": "deadline",
      "severity": "high",
      "assessmentMethod": "deterministic-rules",
      "severityReason": "Deadline language was detected; verify the official date and submission instructions.",
      "proposalSections": ["Submission schedule and compliance checklist"],
      "suggestionsAreHeuristic": true,
      "matchMethod": "token-similarity",
      "before": "Proposals must be received by 15 October 2026 at 17:00 UTC.",
      "after": "Proposals must be received by 22 October 2026 at 17:00 UTC.",
      "citations": {
        "original": {
          "source": "original",
          "name": "Original RFP",
          "url": null,
          "page": 1,
          "quote": "Proposals must be received by 15 October 2026 at 17:00 UTC.",
          "relation": "changed-text"
        },
        "amendment": {
          "source": "amendment",
          "name": "Amendment 1",
          "url": null,
          "page": 1,
          "quote": "Proposals must be received by 22 October 2026 at 17:00 UTC.",
          "relation": "changed-text"
        }
      }
    }
  ],
  "warnings": [],
  "limitations": [
    "Classifications and proposal-section suggestions use deterministic heuristics and require human review."
  ]
}
```

### Live test notes (2026-10-13)

Build 1.0.9 was checked against real public procurement documents:

- **Rhode Island Commerce RFP-2616** (13-page PDF) vs Addendum 1 and Addendum 2 (addendum mode, one run): Addendum 1 returned exactly one change, the corrected proposal deadline ("…corrected to 11:59 pm, Friday, October 16, 2026."), with 9 boilerplate statements omitted; Addendum 2 returned exactly its two corrected question/response deadlines.
- **Siskiyou County RFP 26-2060-01** vs Addendum 1: 14 changes, 10 of them deadlines; each revised schedule row pairs with the same event in the original (proposal due October 6 → October 13, questions → September 30, interviews October 19–21 → October 20).
- **Siskiyou RFP vs an edited full revision** (replacement mode, native text): all five edits found with page citations (two moved deadlines, an added license requirement, a "shall" → "may" change, an insurance amount change) and nothing else.
- An HTML page URL returned an `UNSUPPORTED_DOCUMENT` error and no `result` charge.

Heuristic limits remain: a schedule row whose original label was not extracted may appear as an addition rather than a modification, and short instructions such as "The Estimated Timeline … is deleted and replaced" are kept for review.

### Limitations and review

The comparison operates on extracted statements rather than document layout. Formatting-only changes, images, signatures, scanned pages, and differences lost during extraction may not be detected. Replacement mode compares statement order, so moving unchanged wording can appear as a removal and an addition. PDF table and column reading order can affect matching. Exact matching and token similarity do not understand cross-references, precedence rules, exceptions, negation, or the contractual effect of an amendment.

Review the cited passages in the source documents, inspect warnings, and confirm deadline and requirement changes before updating a proposal. An empty change list means no statement changes were detected within these limits; it is not a compliance determination.

### Billing

Each successful amendment comparison emits one custom `result` event for Apify pay-per-event billing. Successful comparisons with zero detected changes are charged, including identical or empty native-text documents; the charge is not per changed clause. Error records do not emit that event. The price is set in the Actor's Store pricing configuration; consult the Store listing for the current rate and any platform costs.

### Local use and deployment

From this Actor's directory:

```sh
npm install
mkdir -p storage/key_value_stores/default
cp INPUT.example.json storage/key_value_stores/default/INPUT.json
npm start
```

Node.js 24 or newer is required. With the same local input file, an Apify CLI workflow can use `apify run`. Use `apify push` to deploy to your own account. Configure the Store listing and the `result` event price in Apify before offering the Actor for sale; this repository does not set a customer price or publish a Store listing.

### Live example run

This is real output from a cloud run of the Actor's default (prefilled) input on 2026-10-16. Just click **Start** to reproduce it.

- Full example dataset (public, JSON): https://api.apify.com/v2/datasets/qZXUDZVVawHRWOaPE/items?clean=1\&format=json

Input:

```json
{
  "original": {
    "name": "Original RFP",
    "text": "Proposals must be received by 15 October 2026 at 17:00 UTC."
  },
  "amendments": [
    {
      "name": "Amendment 1",
      "text": "Proposals must be received by 22 October 2026 at 17:00 UTC."
    }
  ]
}
```

Output (first item; long arrays shortened):

```json
{
  "status": "success",
  "amendmentIndex": 1,
  "comparisonMode": "replacement",
  "sources": {
    "original": {
      "name": "Original RFP",
      "url": null,
      "type": "text",
      "pageCount": 1,
      "noTextPages": [],
      "statementCount": 1
    },
    "amendment": {
      "name": "Amendment 1",
      "url": null,
      "type": "text",
      "pageCount": 1,
      "noTextPages": [],
      "statementCount": 1
    }
  },
  "summary": {
    "totalChanges": 1,
    "returnedChanges": 1,
    "truncatedChanges": 0,
    "added": 0,
    "removed": 0,
    "modified": 1,
    "requirements": 0,
    "deadlines": 1,
    "other": 0,
    "filteredBoilerplate": 0
  },
  "changes": [
    {
      "id": "change-1",
      "type": "modified",
      "category": "deadline",
      "severity": "high",
      "severityReason": "A date or time differs between the cited passages or is stated as revised; verify the official schedule.",
      "proposalSections": [
        "Submission schedule and compliance checklist"
      ],
      "assessmentMethod": "deterministic-rules",
      "suggestionsAreHeuristic": true,
      "matchMethod": "token-similarity",
      "similarityScore": 0.944,
      "before": "Proposals must be received by 15 October 2026 at 17:00 UTC.",
      "after": "Proposals must be received by 22 October 2026 at 17:00 UTC.",
      "citations": {
        "original": {
          "source": "original",
          "name": "Original RFP",
          "url": null,
          "page": 1,
          "quote": "Proposals must be received by 15 October 2026 at 17:00 UTC.",
          "relation": "changed-text"
        },
        "amendment": {
          "source": "amendment",
          "name": "Amendment 1",
          "url": null,
          "page": 1,
          "quote": "Proposals must be received by 22 October 2026 at 17:00 UTC.",
          "relation": "changed-text"
        }
      }
    }
  ],
  "warnings": [],
  "limitations": [
    "Compares each amendment independently against the original; does not apply a cumulative amendment history.",
    "Whitespace is normalized. Comparison uses extracted statements, not PDF layout, tables, images, or handwritten marks.",
    "Replacement mode compares statement order; moving unchanged wording can appear as a removal and an addition.",
    "…"
  ]
}
```

# Actor input Schema

## `original` (type: `object`):

An object with exactly one of text or url, plus an optional name. Text may be plain text or Markdown; use a form-feed character (\f) to mark page boundaries. URLs must use HTTP or HTTPS and return a PDF or plain-text document; HTML pages are unsupported.

## `amendments` (type: `array`):

An array of 1 to 50 document objects, each with exactly one of text or url and an optional name. Every amendment is compared with the same original, not with the preceding amendment.

## `comparisonMode` (type: `string`):

Replacement compares complete versions and can report added, removed, or modified statements. Addendum treats the amendment as supplemental text and reports statements absent from the original without inferring removals.

## `maxChanges` (type: `integer`):

Limit the returned change list. Summary totals still describe all detected changes; truncatedChanges reports how many were omitted.

## `includeBoilerplate` (type: `boolean`):

Addendum mode omits form fields, page headers/footers, acknowledgment blocks and standard addendum language by default. Set true to return every new statement.

## Actor input object example

```json
{
  "original": {
    "name": "Original RFP",
    "text": "Proposals must be received by 15 October 2026 at 17:00 UTC."
  },
  "amendments": [
    {
      "name": "Amendment 1",
      "text": "Proposals must be received by 22 October 2026 at 17:00 UTC."
    }
  ],
  "comparisonMode": "replacement",
  "maxChanges": 500,
  "includeBoilerplate": false
}
```

# Actor output Schema

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

No description

## `summary` (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 = {
    "original": {
        "name": "Original RFP",
        "text": "Proposals must be received by 15 October 2026 at 17:00 UTC."
    },
    "amendments": [
        {
            "name": "Amendment 1",
            "text": "Proposals must be received by 22 October 2026 at 17:00 UTC."
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("nickatnite79/rfp-amendment-analyzer").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 = {
    "original": {
        "name": "Original RFP",
        "text": "Proposals must be received by 15 October 2026 at 17:00 UTC.",
    },
    "amendments": [{
            "name": "Amendment 1",
            "text": "Proposals must be received by 22 October 2026 at 17:00 UTC.",
        }],
}

# Run the Actor and wait for it to finish
run = client.actor("nickatnite79/rfp-amendment-analyzer").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 '{
  "original": {
    "name": "Original RFP",
    "text": "Proposals must be received by 15 October 2026 at 17:00 UTC."
  },
  "amendments": [
    {
      "name": "Amendment 1",
      "text": "Proposals must be received by 22 October 2026 at 17:00 UTC."
    }
  ]
}' |
apify call nickatnite79/rfp-amendment-analyzer --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nickatnite79/rfp-amendment-analyzer"
        }
    }
}
```

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/ZzhMw12unFrBzITt7/builds/PWg2RImxaTbVuRwfi/openapi.json
