# Competitor Offer and Pricing Data Normalizer (`zinin/us-brand-offer-evidence-normalizer`) Actor

Normalize buyer-supplied offer evidence into review-required delivery candidates. Check submitted-field consistency, preserve terms and attribution metadata, and expose evidence gaps without fetching a source or claiming external verification.

- **URL**: https://apify.com/zinin/us-brand-offer-evidence-normalizer.md
- **Developed by:** [Tim Zinin](https://apify.com/zinin) (community)
- **Categories:** MCP servers, Automation, Marketing
- **Stats:** 2 total users, 1 monthly users, 90.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $8.50 / 1,000 delivered offer normalization candidates

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — 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

## Offer Evidence Normalizer — Review-Required Offer Rows

Normalize buyer-submitted offer, coupon, landing-page, and ad evidence into deterministic machine-readable rows without pretending that syntactic consistency is external verification.

**Built for:** marketing operations, merchandising, affiliate operations, evidence-governance, and data-quality teams that already possess authorized source evidence and need a bounded normalization step before human review.

**Commercial result unit:** one Dataset delivery candidate whose linked `result-found` push is confirmed by an exact named-event counter increase of one. The separate automatic Actor start event also applies. A Dataset candidate can settle free when the counter delta is zero; current-run KVS `OUTPUT`, not the candidate row, is authoritative. Input rows, Dataset reads, rejected evidence, duplicate work, BYOK attempts, and withheld work are not additional result units.

![Offer Evidence Normalizer: buyer input to evidence-backed action](https://raw.githubusercontent.com/TimmyZinin/apify-actor-assets/6cba17accc700a564e82233bb3a0e304ac06ad59/commercial115/us-brand-offer-evidence-normalizer/readme-hero.webp)

### Why this Actor exists

Offer evidence often arrives from several systems with different field names, mixed structured and unstructured terms, duplicate source records, and attribution fields that look more authoritative than they really are. A spreadsheet can standardize the easy columns, but it rarely preserves the exact normalization basis, duplicate conflicts, review boundary, current run identity, or billing state.

This Actor performs the narrow deterministic part:

- validates a closed input contract;
- accepts either bounded inline rows or one buyer-authorized Apify Dataset;
- canonicalizes HTTPS URLs, domains, identifiers, arrays, and terms;
- parses only explicitly supported offer-language patterns;
- compares structured terms with bounded source text and retains conflicts;
- collapses exact duplicate evidence keys while exposing disagreement;
- checks whether submitted attribution fields are internally consistent;
- emits one review-required row for each normalization-complete eligible offer;
- optionally adds bounded labels with the buyer's OpenRouter key;
- records current-run delivery, named-event, work-unit, budget, anomaly, KVS, and replay truth in `OUTPUT`.

It does **not** fetch a submitted source URL, authenticate a merchant, confirm that an offer is live, prove brand ownership, verify a coupon, establish legal rights, estimate conversion, measure demand, or authorize downstream automation.

### Who uses it

- marketing-operations teams that need one closed normalization contract before review;
- merchandising and affiliate teams comparing submitted offer terms without asserting source truth;
- data-governance teams that need duplicate, conflict, attribution, and retention boundaries;
- CRM and warehouse integrators that need stable digests and a current-run delivery receipt;
- agent builders that need explicit evidence gaps and `safeToAutomate:false` instead of a fabricated recommendation.

The buyer supplies the evidence and remains responsible for source rights, purpose, review, and downstream use. This Actor is useful where deterministic normalization is valuable but an external verification claim would be misleading.

### What you get

Every Dataset delivery candidate uses output contract `2.0` and keeps input contract `1.0` explicit. The row records billing eligibility and intent, never final settlement. Important fields include:

### Field dictionary

| Field | Meaning |
|---|---|
| `actorRunId` | Trusted current Apify run binding. |
| `attemptIndex` / `attemptDigest` | Deterministic identity of this run's one delivery attempt for the row. |
| `entity` | Bounded buyer-submitted brand identity projection; not verified identity. |
| `offer` | Normalized offer kind, terms, dedupe key, and normalization digest. |
| `entityAttribution.assessmentStatus` | `submitted_consistent` only when submitted proof fields agree internally. |
| `entityAttribution.externalVerificationPerformed` | Always `false`. The Actor does not inspect the source. |
| `normalizationNeedsReview` | False only when deterministic term normalization is complete and conflict-free. |
| `attributionNeedsReview` | Always true because attribution is not externally verified. |
| `needsReview` | Always true on Dataset delivery candidates. |
| `reviewReasons` | Exact reasons review is still required. |
| `decision.confidence` | Evidence-sufficiency score, basis, and explicit gaps—not an offer-truth probability. |
| `decision.recommendedAction` | Human routing instruction. |
| `decision.safeToAutomate` | Always false. |
| `resultCandidate` | Always true: this row was submitted through the linked Dataset path; it is not a paid-settlement claim. |
| `decision.billing` | Declares eligibility, intent, unit, and `settlementSource: current_run_kvs_output`; it never claims the row was charged. |
| `analysisStatus` / `labels` | Optional BYOK labeling status and bounded labels. |
| `usage` | Always null on Dataset rows; provider usage is stored once at run level in `OUTPUT`. |

### Evidence and boundaries

#### The critical truth boundary

The input phrase `verified_by_source` is retained as **submitted metadata**. It does not mean this Actor contacted or verified that source. When the submitted proof type, domain, source entity ID, URL, and typed identifiers agree under the closed rules, the output says `submitted_consistent`. It also says:

- `verificationScope: submitted_fields_only`;
- `externalVerificationPerformed: false`;
- `safeToAutomateBusinessDecision: false`;
- `attributionNeedsReview: true`;
- `decision.safeToAutomate: false`.

That distinction is deliberate. Internal consistency is useful for quality control, but it is not a merchant authorization, source licence, live-offer check, legal conclusion, or business permission.

### How to run

For a small review, submit bounded inline `rows`. For an existing authorized Apify Dataset, submit its `datasetId`. Keep the returned platform run ID, poll that same run to terminal state, and then read that run's Dataset plus the default KVS `OUTPUT`. Do not start another run merely to discover whether the first one completed.

The default fixture uses inline buyer-supplied evidence, requires no provider secret, and keeps optional BYOK labeling off. Start with that path unless an approved workflow specifically needs the Dataset source or optional labels.

### Input contract

Choose exactly one source:

1. `rows`: one to 100 inline evidence rows; or
2. `datasetId`: one selected Apify Dataset with explicit READ permission.

The Actor never accepts an arbitrary Dataset URL, signed URL, source token, pagination selector, remote host, or generic payload wrapper. A Dataset ID must match the closed syntax. The Dataset path is fixed, one SDK request probes at most 11 records, and the product accepts no more than 10 Dataset items. Inline and Dataset rows pass the same row, string, UTF-8, URL, and normalization contract.

The public input contract remains `1.0`. Output `2.0` is an intentional truth correction: existing v1 input producers remain valid, while generated paid results no longer call buyer assertions externally verified.

![Offer Evidence Normalizer: evidence-to-action workflow](https://raw.githubusercontent.com/TimmyZinin/apify-actor-assets/6cba17accc700a564e82233bb3a0e304ac06ad59/commercial115/us-brand-offer-evidence-normalizer/readme-workflow.webp)

`requestId` is correlation metadata. It is not a cross-run idempotency key. Starting another run is a new commercial execution and can charge the start and result events again.

#### Complete input example

```json
{
  "schemaVersion": "1.0",
  "requestId": "offer-normalize-20260804-001",
  "rows": [
    {
      "entity": {
        "entityId": "brand:demo-outfitters",
        "brandName": "Demo Outfitters",
        "legalName": null,
        "domain": "demo.example",
        "ticker": null,
        "aliases": [],
        "country": "US",
        "sourceEntityIds": ["prefill:demo-outfitters"]
      },
      "evidence": {
        "evidenceType": "landing_page",
        "sourceName": "demo_fixture",
        "sourceRecordId": "offer-001",
        "sourceUrl": "https://demo.example/offers/spring",
        "observedAt": "2026-08-04T08:00:00Z",
        "rawTitle": "Spring offer",
        "rawTermsText": "20% off orders $50+ with code SPRING20. Ends 2026-09-30.",
        "terms": {
          "offerKind": "percent_off",
          "percentOff": 20,
          "minPurchaseAmount": { "value": 50, "currency": "USD" },
          "couponCode": "SPRING20",
          "endAt": "2026-09-30T23:59:59Z"
        },
        "entityAttribution": {
          "status": "verified_by_source",
          "proofType": "first_party_domain",
          "proofValue": "demo.example",
          "sourceEntityId": "prefill:demo-outfitters",
          "proofUrl": "https://demo.example/",
          "proofDomain": "demo.example",
          "merchantId": null,
          "adAccountId": null,
          "officialSourceId": null
        },
        "additionalUrls": []
      }
    },
    {
      "entity": {
        "entityId": "brand:demo-outfitters",
        "brandName": "Demo Outfitters",
        "legalName": null,
        "domain": "demo.example",
        "ticker": null,
        "aliases": [],
        "country": "US",
        "sourceEntityIds": ["prefill:demo-outfitters"]
      },
      "evidence": {
        "evidenceType": "landing_page",
        "sourceName": "demo_fixture",
        "sourceRecordId": "offer-001",
        "sourceUrl": "https://demo.example/offers/spring",
        "observedAt": "2026-08-04T08:00:00Z",
        "rawTitle": "Spring offer",
        "rawTermsText": "20% off orders $50+ with code SPRING20. Ends 2026-09-30.",
        "terms": {
          "offerKind": "percent_off",
          "percentOff": 20,
          "minPurchaseAmount": { "value": 50, "currency": "USD" },
          "couponCode": "SPRING20",
          "endAt": "2026-09-30T23:59:59Z"
        },
        "entityAttribution": {
          "status": "verified_by_source",
          "proofType": "first_party_domain",
          "proofValue": "demo.example",
          "sourceEntityId": "prefill:demo-outfitters",
          "proofUrl": "https://demo.example/",
          "proofDomain": "demo.example",
          "merchantId": null,
          "adAccountId": null,
          "officialSourceId": null
        },
        "additionalUrls": []
      }
    }
  ],
  "options": { "labels": "off", "analysisModel": null, "maxItems": 100 }
}
```

### Normalization rules

Structured `terms` are primary. The bounded parser fills only fields that are absent and only when the source text contains an explicit supported pattern. Supported concepts include percentage and amount discounts, buy-X-get-Y quantities, free shipping, coupon codes, minimum purchase amounts, membership and new-customer restrictions, geography, channels, dates, and exclusions.

The parser does not guess. Phrases such as “special pricing,” “sale,” “up to,” or “from” without an unambiguous supported value remain partial or unparsed. When structured fields and text disagree, the Actor retains a conflict instead of choosing one value. Conflict, partial, unparsed, missing-proof, and mismatched-proof rows are not eligible paid normalization rows.

The canonical evidence key is:

`entityId | sourceName | sourceRecordId | canonicalSourceUrl | offerKind`

Equivalent keys are grouped after deterministic sorting. Exact duplicates collapse into one normalized work unit. Duplicate rows that disagree on terms or submitted attribution become conflicts. The Actor does not use “last write wins,” does not select a preferred source, and does not silently bill multiple copies of the same key.

The derived `normalizationDigest` binds the canonical entity ID, source identity, canonical source URL, offer kind, and normalized terms. A derived `offerId` equals that digest; a supplied offer ID remains explicitly marked as supplied. These digests are integrity bindings, not signatures, source authorization, anonymization, or cross-run purchase protection.

### Happy, partial, and failure output

A happy run writes one settlement-neutral Dataset candidate per eligible normalization and then proves its current-run settlement in KVS `OUTPUT`. A bounded partial run preserves already confirmed work plus the exact stop reason. A pre-delivery failure writes no candidate; a post-push unknown or anomalous state is never presented as a clean retry.

#### Complete Dataset delivery candidate

The fixture below is the executable output-v2 shape used by schema and semantic tests.

```json
{
  "schemaVersion": "2.0",
  "inputSchemaVersion": "1.0",
  "requestId": "offer-normalize-20260804-001",
  "requestDigest": "sha256:b922715033f82998b5066e9369f29c31021f004f25561ad5dc6fd1bba57ca92c",
  "inputDigest": "sha256:41e292e4323d4b0a85d5134d7e3485912daa24b330bcdd9fc15a056ded8db126",
  "actorRunId": "example-run-001",
  "attemptIndex": 1,
  "attemptDigest": "sha256:2ee444a419726dacd335cb609c4fe7ff5d0639882d6916857e94b6ab5aa1d5bf",
  "resultCandidate": true,
  "entity": {
    "entityId": "brand:demo-outfitters",
    "brandName": "Demo Outfitters",
    "legalName": null,
    "domain": "demo.example",
    "ticker": null,
    "aliases": [],
    "sourceEntityIds": ["prefill:demo-outfitters"],
    "country": "US"
  },
  "eventType": "offer_evidence",
  "eventId": "offer-001",
  "observedAt": "2026-08-04T08:00:00Z",
  "effectiveAt": null,
  "sourceName": "demo_fixture",
  "sourceUrl": "https://demo.example/offers/spring",
  "sourceRecordId": "offer-001",
  "confidence": null,
  "offer": {
    "offerId": "sha256:04ce7e6a1a35568be96af2eb241cd83be44264bfbda0e05d3c5c6aa3ce18751b",
    "offerIdSource": "derived",
    "normalizationDigest": "sha256:04ce7e6a1a35568be96af2eb241cd83be44264bfbda0e05d3c5c6aa3ce18751b",
    "offerKind": "percent_off",
    "normalizationStatus": "complete",
    "terms": {
      "percentOff": 20,
      "amountOff": null,
      "buyQuantity": null,
      "getQuantity": null,
      "minPurchaseAmount": { "value": 50, "currency": "USD" },
      "couponCode": "SPRING20",
      "couponCodeCanonical": "SPRING20",
      "memberOnly": null,
      "newCustomerOnly": null,
      "startAt": null,
      "endAt": "2026-09-30T23:59:59Z",
      "geography": null,
      "channels": null,
      "exclusions": null
    },
    "termConflicts": [],
    "dedupeKey": "brand:demo-outfitters|demo_fixture|offer-001|https://demo.example/offers/spring|percent_off"
  },
  "entityAttribution": {
    "assessmentStatus": "submitted_consistent",
    "submittedStatus": "verified_by_source",
    "verificationScope": "submitted_fields_only",
    "externalVerificationPerformed": false,
    "safeToAutomateBusinessDecision": false,
    "proofType": "first_party_domain",
    "proofValue": "demo.example",
    "sourceEntityId": "prefill:demo-outfitters",
    "proofUrl": "https://demo.example/",
    "proofDomain": "demo.example",
    "merchantId": null,
    "adAccountId": null,
    "officialSourceId": null
  },
  "evidence": {
    "evidenceType": "landing_page",
    "sourceName": "demo_fixture",
    "sourceRecordId": "offer-001",
    "sourceUrl": "https://demo.example/offers/spring",
    "observedAt": "2026-08-04T08:00:00Z",
    "rawTitle": "Spring offer",
    "rawTermsText": "20% off orders $50+ with code SPRING20. Ends 2026-09-30.",
    "sourceTerms": {
      "offerKind": "percent_off",
      "percentOff": 20,
      "minPurchaseAmount": { "value": 50, "currency": "USD" },
      "couponCode": "SPRING20",
      "endAt": "2026-09-30T23:59:59Z"
    },
    "additionalUrls": [],
    "evidenceTruncated": false
  },
  "normalizationNeedsReview": false,
  "attributionNeedsReview": true,
  "needsReview": true,
  "reviewReasons": ["external_attribution_not_verified"],
  "analysisStatus": "skipped_no_key",
  "analysisModel": null,
  "usage": null,
  "usageScope": "run",
  "labels": null,
  "decision": {
    "entityId": "offer:sha256:04ce7e6a1a35568be96af2eb241cd83be44264bfbda0e05d3c5c6aa3ce18751b",
    "eventId": "offer-evidence:sha256:04ce7e6a1a35568be96af2eb241cd83be44264bfbda0e05d3c5c6aa3ce18751b",
    "observedAt": "2026-08-04T08:00:00Z",
    "freshness": { "status": "unknown", "observedAt": "2026-08-04T08:00:00Z", "basis": "buyer_submitted_observed_at" },
    "change": { "status": "not_measured", "basis": "single_run_normalization" },
    "confidence": {
      "level": "medium",
      "score": 65,
      "basis": ["submitted_offer_fields_normalized", "submitted_attribution_fields_internally_consistent"],
      "gaps": ["external_offer_not_fetched", "external_attribution_not_verified", "source_rights_not_verified", "business_effect_not_verified"]
    },
    "evidence": {
      "requestDigest": "sha256:b922715033f82998b5066e9369f29c31021f004f25561ad5dc6fd1bba57ca92c",
      "inputDigest": "sha256:41e292e4323d4b0a85d5134d7e3485912daa24b330bcdd9fc15a056ded8db126",
      "normalizationDigest": "sha256:04ce7e6a1a35568be96af2eb241cd83be44264bfbda0e05d3c5c6aa3ce18751b",
      "attemptDigest": "sha256:2ee444a419726dacd335cb609c4fe7ff5d0639882d6916857e94b6ab5aa1d5bf"
    },
    "recommendedAction": "REVIEW_SUBMITTED_OFFER_AND_ATTRIBUTION_BEFORE_USE",
    "priority": "normal",
    "safeToAutomate": false,
    "summary": "Offer fields normalized from buyer-submitted evidence; external attribution and offer truth were not verified.",
    "failureDiagnostics": null,
    "billing": { "billingEligible": true, "billingIntent": "linked_push_result_found", "eventName": "result-found", "unit": "normalized_offer_delivery_candidate", "settlementSource": "current_run_kvs_output" }
  },
  "deliveryIntent": { "event": "result-found", "businessRows": 1 }
}
```

### Decision routing

Each delivered row includes an additive decision object designed for evidence review:

- stable `entityId` and `eventId` derived from the normalized offer ID;
- buyer-submitted `observedAt`;
- freshness status `unknown` with basis `buyer_submitted_observed_at`;
- change status `not_measured` because one stateless run has no prior observation;
- medium confidence score 65 based on completed normalization and internally consistent submitted proof fields;
- explicit gaps for external offer checks, attribution verification, source rights, and business effect;
- request, input, normalization, and attempt digests;
- recommended action `REVIEW_SUBMITTED_OFFER_AND_ATTRIBUTION_BEFORE_USE`;
- priority `normal`;
- `safeToAutomate:false`;
- null failure diagnostics on a valid delivered row;
- one declared result billing unit.

The confidence score describes only the evidence available to this Actor. It is not accuracy, merchant authenticity, redemption probability, offer availability, legal validity, conversion likelihood, or expected revenue. A downstream system must not raise that score because a brand name looks familiar or because a URL is syntactically valid.

### KVS OUTPUT

Read the default Key-Value Store record named `OUTPUT` after every run. Dataset rows answer “what normalizations were delivered?” `OUTPUT` answers “what happened in this exact run?”

`OUTPUT` includes:

- accepted input and output schema versions;
- Actor name, current run ID, request ID, and request digest;
- terminal status and whether any named paid result was confirmed;
- input, deduped, submitted-consistent, eligible, and rejected work-unit counts;
- attempted pushes, confirmed Dataset writes, exact named result events, paid rows, free rows, anomalous rows, unknown operations, last receipt, and last attempt identity;
- requested, unique, duplicate, successful, failed, delivered, paid, free, anomalous, withheld, and unknown run counts;
- partial, budget, fatal, replay, and automation flags;
- input source and Dataset request statistics;
- output limit and truncation truth;
- optional provider analysis status and run-level token usage;
- bounded errors;
- primary and recovery KVS states plus requested/failed exit state.

#### Complete successful OUTPUT example

```json
{
  "schemaVersion": "2.0",
  "inputSchemaVersion": "1.0",
  "actor": "us-brand-offer-evidence-normalizer",
  "runId": "example-run-001",
  "requestId": "offer-normalize-20260804-001",
  "requestDigest": "sha256:b922715033f82998b5066e9369f29c31021f004f25561ad5dc6fd1bba57ca92c",
  "status": "found",
  "resultFound": true,
  "counts": { "input": 2, "deduped": 1, "submittedConsistent": 1, "eligible": 1, "rejected": 0 },
  "delivery": {
    "attemptedPushCount": 1,
    "confirmedDatasetWrites": 1,
    "confirmedResultEvents": 1,
    "confirmedCharges": 1,
    "freeDatasetWrites": 0,
    "anomalousDatasetWrites": 0,
    "anomalousSettlementCount": 0,
    "anomalousChargeCount": null,
    "unknownDatasetWrites": 0,
    "unknownSettlements": 0,
    "lastReceiptId": "fixture-receipt-1",
    "lastAttempt": {
      "attemptIndex": 1,
      "attemptDigest": "sha256:2ee444a419726dacd335cb609c4fe7ff5d0639882d6916857e94b6ab5aa1d5bf",
      "offerId": "sha256:04ce7e6a1a35568be96af2eb241cd83be44264bfbda0e05d3c5c6aa3ce18751b",
      "state": "confirmed_paid"
    }
  },
  "run": {
    "requestedCount": 2,
    "uniqueCount": 1,
    "duplicateCount": 1,
    "successfulCount": 1,
    "failedCount": 0,
    "deliveredRowCount": 1,
    "paidRowCount": 1,
    "freeRowCount": 0,
    "anomalousDeliveryCount": 0,
    "withheldRowCount": 0,
    "unknownDeliveryCount": 0,
    "unknownSettlementCount": 0,
    "partial": false,
    "budgetStopped": false,
    "fatalError": false,
    "replaySafe": false,
    "safeToAutomate": false
  },
  "source": { "kind": "inline", "datasetId": null },
  "limits": { "maxItems": 100, "truncated": false },
  "analysisStatus": "skipped_no_key",
  "analysisModel": null,
  "usage": null,
  "usageScope": "run",
  "sourceStats": { "datasetRequests": 0, "decodedBytes": 0, "elapsedMs": 12 },
  "errors": [],
  "replaySafe": false,
  "terminal": {
    "outcome": "SUCCEEDED",
    "failureStage": null,
    "retryable": false,
    "primaryKvsWrite": "confirmed",
    "recoveryKvsWrite": "not_attempted",
    "exit": "requested"
  }
}
```

#### Status matrix

| Status | Meaning | Retry boundary |
|---|---|---|
| `found` | Every eligible row within the result limit was delivered and its named result event was confirmed. | A new run is a new purchase; `replaySafe:false`. |
| `no_match` | Normalization produced no eligible delivered row. | No Dataset push was attempted; inspect counts and evidence problems. |
| `result_limit_reached` | Exactly `maxItems` eligible rows were delivered and more eligible rows existed. | This is not a budget error. Increase the declared limit only if another purchase is intended. |
| `budget_stopped` | The next result did not fit the buyer cap or a linked push was confirmed free by an exact zero named-event delta. | Inspect paid/free/withheld counts; do not call it a result-limit stop. |
| `partial` | Some paid rows were confirmed before a deterministic pricing or row failure stopped the run. | Preserve the receipt and correct the named failure before deciding on another run. |
| `unknown_delivery` | The linked push threw, so Dataset delivery itself is unknown. | Never retry blindly. Reconcile the original Dataset, PPE counters, KVS, and logs. |
| `settlement_unknown` | The linked push returned, proving one Dataset write, but the post-push named counter was unreadable. | Treat delivery as known and payment as unresolved; never retry blindly. |
| `settlement_anomaly` | The named counter and aggregate receipt contradicted the expected single-unit settlement. | Preserve the original run. Named-counter truth is retained; manual reconciliation is required. |
| Input/source/pricing failure | Failure was established before any Dataset push. | The receipt is replay-safe with respect to result delivery, but another run still has a new start event. |

### Pricing

The live Apify pricing panel is the current source of truth. The configured contract accepts exactly these six paired tiers:

| Tier | Actor start | One delivered normalization | Start + one result\* |
|---|---:|---:|---:|
| FREE | $0.00500 | $0.01000 | $0.01500 |
| BRONZE | $0.00475 | $0.00950 | $0.01425 |
| SILVER | $0.00450 | $0.00900 | $0.01350 |
| GOLD | $0.00425 | $0.00850 | $0.01275 |
| PLATINUM | $0.00410 | $0.00820 | $0.01230 |
| DIAMOND | $0.00400 | $0.00800 | $0.01200 |

\*Configured event prices only. Platform credits, taxes, storage, account agreements, optional OpenRouter usage, and other platform consumption are not included.

Mixed tiers, missing events, additional events, a priced default Dataset-item event, malformed values, or a platform-spent amount that does not include the resolved start event fail closed before delivery. A valid positive infinite platform cap is treated as unlimited. Finite caps use integer micro-USD arithmetic.

Before every linked push, the Actor rereads pricing and budget state. It then reads `getChargedEventCount('result-found')` and requires it to equal the number of already confirmed paid rows. One `pushData(row, 'result-found')` follows. The named counter is read again:

- exact delta +1: one paid result confirmed;
- exact delta 0: one free Dataset write confirmed;
- any other readable delta: settlement anomaly;
- thrown linked push: Dataset delivery is unknown, so the run stops without retry and records `unknown_delivery`.
- returned linked push plus an unreadable post-push named counter: Dataset delivery is known, settlement is unknown, and the run records `settlement_unknown`.

The push is never retried inside the run. Aggregate SDK receipts are retained as a second consistency signal, but a vague positive aggregate cannot replace the exact named-event delta.

### Bring your own key (BYOK)

BYOK labels are optional. Set `options.labels` to `byok`, provide an explicit OpenRouter model, and supply `openrouterApiKey`. The deterministic normalization product remains independent of those labels.

The Actor sends at most one bounded request to the fixed OpenRouter chat-completions endpoint. The request contains only normalized offer IDs, offer kinds, and normalized terms for the rows planned for delivery. It does not send arbitrary instructions, tools, source-page content, images, or a browsing request. A response may add only one closed offer category and audience label for every selected offer ID.

Provider errors, timeouts, malformed output, missing labels, or request-cap skips cannot change offer facts, attribution assessment, dedupe, eligibility, delivery, or billing. Dataset rows contain `usage:null`; aggregate token usage appears once in run-level `OUTPUT`.

The key is marked secret in the Input UI and excluded from request digests, Dataset rows, KVS `OUTPUT`, and application logs. Apify still stores Actor input in the run's INPUT record according to workspace retention. Restrict run access and delete stored runs when the approved purpose ends. OpenRouter receives the bounded projection under the buyer's provider agreement, subprocessors, logging, and retention settings; this Actor does not promise zero retention.

### Integration recipes

Start one run, poll that exact run, then read its exact Dataset and KVS. Do not create another run as a polling mechanism.

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/zinin~us-brand-offer-evidence-normalizer/runs?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @actors/us-brand-offer-evidence-normalizer/fixtures/inline-prefill.json
```

In an integration, require all of the following before accepting a paid result:

1. platform run ID equals `OUTPUT.runId`;
2. output schema is `2.0` and input schema is `1.0`;
3. Dataset row count equals `run.deliveredRowCount`;
4. named `result-found` count equals `run.paidRowCount`;
5. each Dataset delivery candidate has the same `actorRunId`;
6. every attempt index and attempt digest is unique within the run;
7. no unexpected paid Dataset event exists;
8. `unknownDeliveryCount`, `unknownSettlementCount`, and anomaly counts are zero for ordinary success;
9. `safeToAutomate` remains false;
10. a reviewer follows the declared recommended action and evidence gaps.

#### Accepted historical run evidence

These two redacted summaries bind the Actor's previously accepted inline and authorized-Dataset production proofs. They demonstrate real platform delivery and PPE settlement on the listed legacy build; they are not examples of the replacement output 2.0 candidate that still requires its own no-retry canary.

```json
{
  "runId": "Rae3RnIRp2bkhFm0f",
  "buildId": "YblzxHduCD2mtSX8K",
  "status": "SUCCEEDED",
  "evidenceAccepted": true,
  "acceptanceScope": "historical input/output 1.0 production proof",
  "inputSource": "inline",
  "chargedEventCounts": {
    "apify-actor-start": 1,
    "result-found": 1
  }
}
```

```json
{
  "runId": "g3w1LQmXSFsJM0FrV",
  "buildId": "YblzxHduCD2mtSX8K",
  "status": "SUCCEEDED",
  "evidenceAccepted": true,
  "acceptanceScope": "historical input/output 1.0 production proof",
  "inputSource": "authorized_dataset",
  "chargedEventCounts": {
    "apify-actor-start": 1,
    "result-found": 1
  }
}
```

### Commercial playbooks

**Merchandising review:** normalize authorized offer evidence, reject conflicts, then route every delivered candidate to a human who checks the authoritative offer source.

**Affiliate operations:** use the dedupe key and digests to detect repeated submitted evidence before starting another paid run; never translate internal consistency into merchant approval.

**CRM or warehouse ingestion:** store Dataset candidates with their run ID, then join only to the same run's KVS settlement receipt. Keep unknown and anomalous outcomes out of automated activation.

**Agent-assisted review:** allow an agent to summarize normalized terms and evidence gaps, but require it to retain `safeToAutomate:false`, cite the buyer-supplied source identity, and stop on an unresolved delivery state.

An agent can use this Actor as a bounded evidence-normalization tool, not as an offer-verification authority. Configure the agent to preserve the exact run ID and digests, quote the evidence gaps, and route the result to review. Never allow an agent to translate `submitted_consistent` into “verified,” “official,” “live,” “valid,” or “authorized.”

For scheduled runs, remember that the Actor is stateless across runs. Canonical digests help compare normalized content, but they do not prevent buying the same result twice. Maintain business idempotency before starting a run if duplicate purchases are unacceptable.

If a run times out in the caller, poll the same run ID. If the run reports unknown Dataset delivery, unknown settlement after a known Dataset write, or a settlement anomaly, stop. Inspect the original Dataset, KVS, charged-event counts, logs, and attempt identity. A second run is not a safe reconciliation strategy.

### Sources and rights

This Actor fetches no submitted evidence URL and has no third-party source licence of its own. That does not grant the buyer rights to process or redistribute submitted data. The buyer must have an approved purpose and the rights to submit every brand field, offer term, source record, URL, timestamp, and proof field.

Do not include credentials, signed URLs, customer lists, personal contacts, device identifiers, advertising identifiers, confidential notes, or unnecessary free text. A hash or opaque ID can still be personal or confidential when another system can relink it. Hashing is not anonymization.

Use workspace access controls and a defined retention schedule for Input, Dataset, KVS, and logs. Preserve upstream correction and deletion obligations. This Actor does not contact a source, notify a data subject, verify consent, determine controller/processor roles, or delete data in another system.

The output is not legal advice, a trademark conclusion, a platform-policy decision, a price guarantee, or authorization for targeting, outreach, purchasing, ranking, eligibility, or enforcement.

#### Failure model

#### Input rejected

Read `OUTPUT.errors` and `terminal.failureStage`. Common causes are source XOR violations, unknown fields, oversized input, invalid Dataset IDs, unsupported schema version, malformed dates/URLs, term conflicts, proof mismatches, and invalid BYOK relations. Closed contracts reject unknown fields instead of silently dropping them.

#### Selected Dataset unavailable

The Actor distinguishes not found, forbidden, malformed, redirected, oversized, truncated, timed-out, and transport failures. These states produce no paid normalization row. The run-level receipt binds the attempted Dataset ID and request statistics without echoing a credential.

#### No eligible row

`no_match` means the submitted work produced no normalization-complete, submitted-consistent row. It does not prove that no offer exists. Inspect normalization and attribution reasons in the source system; rejected rows are not republished as Dataset delivery candidates.

#### Result limit reached

`result_limit_reached` means the declared `maxItems` was reached. It is deliberately separate from `budget_stopped`. The receipt shows exactly how many eligible rows were paid and how many were withheld.

#### Primary OUTPUT write fails

The runtime makes one bounded recovery write. A persisted recovery receipt says primary KVS write failed, recovery write confirmed, exit failed, and includes `output_write_failed`. If both writes fail, the platform run fails without pretending a receipt exists.

### Operating guide

Before using a row, verify outside this Actor:

- the source was authorized for this purpose;
- the source record still exists and is current;
- the merchant, brand, and proof identifiers belong together;
- the offer terms match the current authoritative source;
- dates, geography, channels, exclusions, and audience restrictions are complete;
- coupon or promotion availability is confirmed where necessary;
- source rights permit downstream storage and use;
- personal and confidential data were minimized;
- the reviewer understands that `observedAt` is buyer submitted;
- the downstream action is proportionate and human approved.

### FAQ

#### Does `submitted_consistent` mean verified?

No. It means only that the submitted proof fields satisfy the closed internal relations. The Actor explicitly records that no external verification was performed.

#### Does the Actor visit the evidence URL?

No. It canonicalizes and records HTTPS URLs but does not fetch, resolve, render, authenticate, malware-scan, or verify them.

#### Are incomplete or conflicting rows charged?

No `result-found` event is requested for rows that are normalization-incomplete, conflicted, missing proof, mismatched, duplicate-only, withheld before delivery, or rejected by contract.

#### Can a complete normalization still require review?

Yes. Every delivered row requires attribution and source review. Normalization completeness and external truth are different questions.

#### Can BYOK labels alter eligibility or billing?

No. Labels are an optional bounded presentation layer. Provider failures do not change deterministic facts, eligibility, delivery, or billing decisions.

#### Is rerunning the same request ID idempotent?

No. Each run is a new commercial execution. Use your own pre-run idempotency control and compare digests before starting another run.

#### Why is provider usage absent from each Dataset row?

One provider call can label several rows. Repeating the same aggregate token count on every row would overstate usage. It appears once in KVS `OUTPUT`.

#### Can I automate campaign or pricing changes from the output?

No. `safeToAutomate` is false. Review the authoritative source, rights, terms, attribution, and business context first.

### Related Actors

| Public Actor | Use it for |
|---|---|
| [Shopify Store Intelligence](https://apify.com/zinin/shopify-store-intelligence) | Authorized storefront evidence collection before normalization. |
| [Shopify Price Change Monitor](https://apify.com/zinin/shopify-price-change-monitor) | Source-specific price-change evidence with its own contract. |
| [Structured Extract](https://apify.com/zinin/structured-extract) | Bounded structured extraction from buyer-authorized pages. |
| [Intent Signal Aggregator](https://apify.com/zinin/intent-signal-aggregator) | Aggregate already authorized evidence into a separate signal product. |
| [Social Preview Checker](https://apify.com/zinin/social-preview-checker) | Inspect authorized page preview metadata before downstream review. |

Use each Actor only inside its own source-rights, privacy, billing, and completeness boundary. A crosslink is not evidence that two outputs are automatically compatible.

# Actor input Schema

## `schemaVersion` (type: `string`):

Required V1 contract version.

## `requestId` (type: `string`):

Correlation ID for this run. Reusing it in another run does not deduplicate purchases.

## `rows` (type: `array`):

One to 100 closed evidence-row objects. Leave empty when selecting a Dataset.

## `datasetId` (type: `string`):

One resource selected with READ permission. Dataset V1 accepts at most 10 rows and probes 11 in one SDK call. Leave empty when using inline rows.

## `openrouterApiKey` (type: `string`):

Optional caller-owned key for one bounded OpenRouter labeling call. It is secret-masked in the UI and excluded from Dataset/OUTPUT/logs and request digests, but Apify stores Actor INPUT for the run; delete or restrict run storage according to your policy.

## `options` (type: `object`):

Labels are off unless an explicit model and caller key are supplied in BYOK mode.

## Actor input object example

```json
{
  "schemaVersion": "1.0",
  "requestId": "offer-normalize-20260804-001",
  "rows": [
    {
      "entity": {
        "entityId": "brand:demo-outfitters",
        "brandName": "Demo Outfitters",
        "legalName": null,
        "domain": "demo.example",
        "ticker": null,
        "aliases": [],
        "country": "US",
        "sourceEntityIds": [
          "prefill:demo-outfitters"
        ]
      },
      "evidence": {
        "evidenceType": "landing_page",
        "sourceName": "demo_fixture",
        "sourceRecordId": "offer-001",
        "sourceUrl": "https://demo.example/offers/spring",
        "observedAt": "2026-08-04T08:00:00Z",
        "rawTitle": "Spring offer",
        "rawTermsText": "20% off orders $50+ with code SPRING20. Ends 2026-09-30.",
        "terms": {
          "offerKind": "percent_off",
          "percentOff": 20,
          "minPurchaseAmount": {
            "value": 50,
            "currency": "USD"
          },
          "couponCode": "SPRING20",
          "endAt": "2026-09-30T23:59:59Z"
        },
        "entityAttribution": {
          "status": "verified_by_source",
          "proofType": "first_party_domain",
          "proofValue": "demo.example",
          "sourceEntityId": "prefill:demo-outfitters",
          "proofUrl": "https://demo.example/",
          "proofDomain": "demo.example",
          "merchantId": null,
          "adAccountId": null,
          "officialSourceId": null
        },
        "additionalUrls": []
      }
    }
  ],
  "options": {
    "labels": "off",
    "maxItems": 100
  }
}
```

# Actor output Schema

## `OUTPUT` (type: `string`):

No description

## `results` (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 = {
    "schemaVersion": "1.0",
    "requestId": "offer-normalize-20260804-001",
    "rows": [
        {
            "entity": {
                "entityId": "brand:demo-outfitters",
                "brandName": "Demo Outfitters",
                "legalName": null,
                "domain": "demo.example",
                "ticker": null,
                "aliases": [],
                "country": "US",
                "sourceEntityIds": [
                    "prefill:demo-outfitters"
                ]
            },
            "evidence": {
                "evidenceType": "landing_page",
                "sourceName": "demo_fixture",
                "sourceRecordId": "offer-001",
                "sourceUrl": "https://demo.example/offers/spring",
                "observedAt": "2026-08-04T08:00:00Z",
                "rawTitle": "Spring offer",
                "rawTermsText": "20% off orders $50+ with code SPRING20. Ends 2026-09-30.",
                "terms": {
                    "offerKind": "percent_off",
                    "percentOff": 20,
                    "minPurchaseAmount": {
                        "value": 50,
                        "currency": "USD"
                    },
                    "couponCode": "SPRING20",
                    "endAt": "2026-09-30T23:59:59Z"
                },
                "entityAttribution": {
                    "status": "verified_by_source",
                    "proofType": "first_party_domain",
                    "proofValue": "demo.example",
                    "sourceEntityId": "prefill:demo-outfitters",
                    "proofUrl": "https://demo.example/",
                    "proofDomain": "demo.example",
                    "merchantId": null,
                    "adAccountId": null,
                    "officialSourceId": null
                },
                "additionalUrls": []
            }
        }
    ],
    "options": {
        "labels": "off",
        "maxItems": 100
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("zinin/us-brand-offer-evidence-normalizer").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 = {
    "schemaVersion": "1.0",
    "requestId": "offer-normalize-20260804-001",
    "rows": [{
            "entity": {
                "entityId": "brand:demo-outfitters",
                "brandName": "Demo Outfitters",
                "legalName": None,
                "domain": "demo.example",
                "ticker": None,
                "aliases": [],
                "country": "US",
                "sourceEntityIds": ["prefill:demo-outfitters"],
            },
            "evidence": {
                "evidenceType": "landing_page",
                "sourceName": "demo_fixture",
                "sourceRecordId": "offer-001",
                "sourceUrl": "https://demo.example/offers/spring",
                "observedAt": "2026-08-04T08:00:00Z",
                "rawTitle": "Spring offer",
                "rawTermsText": "20% off orders $50+ with code SPRING20. Ends 2026-09-30.",
                "terms": {
                    "offerKind": "percent_off",
                    "percentOff": 20,
                    "minPurchaseAmount": {
                        "value": 50,
                        "currency": "USD",
                    },
                    "couponCode": "SPRING20",
                    "endAt": "2026-09-30T23:59:59Z",
                },
                "entityAttribution": {
                    "status": "verified_by_source",
                    "proofType": "first_party_domain",
                    "proofValue": "demo.example",
                    "sourceEntityId": "prefill:demo-outfitters",
                    "proofUrl": "https://demo.example/",
                    "proofDomain": "demo.example",
                    "merchantId": None,
                    "adAccountId": None,
                    "officialSourceId": None,
                },
                "additionalUrls": [],
            },
        }],
    "options": {
        "labels": "off",
        "maxItems": 100,
    },
}

# Run the Actor and wait for it to finish
run = client.actor("zinin/us-brand-offer-evidence-normalizer").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 '{
  "schemaVersion": "1.0",
  "requestId": "offer-normalize-20260804-001",
  "rows": [
    {
      "entity": {
        "entityId": "brand:demo-outfitters",
        "brandName": "Demo Outfitters",
        "legalName": null,
        "domain": "demo.example",
        "ticker": null,
        "aliases": [],
        "country": "US",
        "sourceEntityIds": [
          "prefill:demo-outfitters"
        ]
      },
      "evidence": {
        "evidenceType": "landing_page",
        "sourceName": "demo_fixture",
        "sourceRecordId": "offer-001",
        "sourceUrl": "https://demo.example/offers/spring",
        "observedAt": "2026-08-04T08:00:00Z",
        "rawTitle": "Spring offer",
        "rawTermsText": "20% off orders $50+ with code SPRING20. Ends 2026-09-30.",
        "terms": {
          "offerKind": "percent_off",
          "percentOff": 20,
          "minPurchaseAmount": {
            "value": 50,
            "currency": "USD"
          },
          "couponCode": "SPRING20",
          "endAt": "2026-09-30T23:59:59Z"
        },
        "entityAttribution": {
          "status": "verified_by_source",
          "proofType": "first_party_domain",
          "proofValue": "demo.example",
          "sourceEntityId": "prefill:demo-outfitters",
          "proofUrl": "https://demo.example/",
          "proofDomain": "demo.example",
          "merchantId": null,
          "adAccountId": null,
          "officialSourceId": null
        },
        "additionalUrls": []
      }
    }
  ],
  "options": {
    "labels": "off",
    "maxItems": 100
  }
}' |
apify call zinin/us-brand-offer-evidence-normalizer --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,zinin/us-brand-offer-evidence-normalizer"
        }
    }
}

```

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/WwMiW0TKaE0WQC2jg/builds/Pp5nhoKyG3YFj9xl7/openapi.json
