# Maps/Leads Novelty Gate (`fg-ei/maps-leads-novelty-gate`) Actor

Run after each Google Maps or leads scrape: keeps a persistent placeId/email history in your account and outputs only leads never seen before. No scraping.

- **URL**: https://apify.com/fg-ei/maps-leads-novelty-gate.md
- **Developed by:** [FG EI](https://apify.com/fg-ei) (community)
- **Categories:** Lead generation, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.025 / novelty report

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

## Maps/Leads Novelty Gate

Après un scrape Maps ou leads, ne garder que les fiches (`placeId` / email) **jamais vues** dans ton historique Apify, avec un échec optionnel si le taux de nouveauté est trop bas.

Brouillon local. Pas publié sur le Store. Pas de pricing live.

Ce n’est pas un déduplicateur intra-batch, pas un filtre de champs, pas un scraper. Aucun fetch Google Maps. L’historique vit dans **ton** Dataset ou key-value store, jamais dans un stockage partagé.

### Commandes

```bash
npm install
npm test
npm run build
npm run smoke:local
```

`npm test` lance Vitest (seed, second run à 0 NEW, placeId, email, auto, ids invalides, doublons, gate PASS/FAIL, dataset vide, stockage, pas d’exfiltration).

`npm run smoke:local` enchaîne deux runs du binaire (`dist/main.js`) sur le stockage local : seed puis 0 NEW.

### Lancer avec l’input Apify

```bash
npm run build
mkdir -p storage/key_value_stores/default
cp store/examples/input-inline-seed.json storage/key_value_stores/default/INPUT.json
APIFY_LOCAL_STORAGE_DIR="$PWD/storage" CRAWLEE_STORAGE_DIR="$PWD/storage" npm start
```

Le rapport est dans `storage/key_value_stores/default/NOVELTY_REPORT.json`. L’historique seed est dans `storage/key_value_stores/maps-leads-novelty-history/NOVELTY_HISTORY.json`.

Sur la plateforme Apify (compte utilisateur, après un `apify push` privé) :

```bash
npm install -g apify-cli
apify login
apify push
```

`apify push` met à jour un Actor privé. Il ne publie pas le Store et n’applique pas les prix. Ne pas cliquer sur Publish. Ne pas ouvrir Monetization. Détail : `store/PUBLISH_CHECKLIST.md`.

### Entrées

| Champ | Défaut | Rôle |
| --- | --- | --- |
| `datasetId` **ou** `items` | — | Dataset du scrape courant, ou lignes inline. Un seul des deux. |
| `historyDatasetId` **ou** `historyKeyValueStoreId` | store nommé | Historique dans **ton** compte. Les deux ensemble = erreur. |
| `historyKey` | `NOVELTY_HISTORY` | Clé KV. Ignorée sans id de store, sauf si tu mets une clé custom (alors l’id est obligatoire). |
| `identityMode` | `auto` | `placeId`, `email`, ou `auto`. |
| `emitMode` | `new_only` | `seed_history_only` n’émet rien. `new_and_changed` est **différé** et fait échouer le run. |
| `failIfNoveltyRateBelow` | non défini | 0–1. Échec si le taux est **strictement inférieur**. Ignoré au seed. |
| `updateHistory` | `true` | Écrit les NEW. Un gate en échec n’écrit pas. |
| `normalizeEmail` | `true` | minuscules, trim, suppression de `%20` et des espaces. |

`auto` : `placeId` (aussi `place_id`, `googlePlaceId`) s’il est présent, sinon un email par adresse (`email` / `emails[]`), sinon repli moins fiable : hash `name` + `address`, ou téléphone déjà en E.164. Le repli ne s’applique pas en mode `placeId` ou `email`. Aucun indicatif pays n’est deviné.

### Sorties

- Dataset du run : uniquement les lignes **NEW** (vide au premier run, en `seed_history_only`, et si le gate échoue). Champs ajoutés : `noveltyStatus`, `noveltyIdentityType`, `noveltyIdentityKey`.
- Key-value du run, clé `NOVELTY_REPORT` : `itemsScanned`, `newCount`, `knownCount`, `skippedNoIdentity`, `intraRunDupes`, `noveltyRate`, `identityModeUsed`, `historyUpdated`, `firstRun`, plus l’état du gate et les warnings.

`noveltyRate = newCount / (newCount + knownCount)`. Au seed, le taux est `null` (pas 1.0).

### Historique et premier run

Premier run (pas d’id, ou store/dataset accessible mais vide) : toutes les identités vont dans l’historique, **aucune** fiche n’est émise, le run réussit. Le rapport indique où écrire l’id pour la suite.

Sans id, le seed ouvre ou crée le key-value store nommé `maps-leads-novelty-history` (clé `NOVELTY_HISTORY`) dans le compte du run. Le run suivant sans id réutilise ce nom.

Un id fourni mais inaccessible, ou un document d’historique illisible, **échoue**. On ne traite pas un historique cassé comme « tout est nouveau ».

Contenu stocké : `{ identityKey, identityType, firstSeenAt, lastSeenAt }`. Pas la fiche entière. Un email utilisé comme identité reste dans ton stockage : c’est le point de comparaison, il ne sort pas du compte.

### Plafonds

Plus de 100 000 lignes en entrée, ou plus de 500 000 enregistrements d’historique : le run échoue, sans couper en silence.

### Prix (proposition seulement)

`PPE_SAFE_TO_APPLY = NO`. Voir `store/PPE_PROPOSAL.md`.

- `apify-actor-start` $0.00005 (événement plateforme, pas chargé dans le code)
- `items-scanned` $0.00005 / ligne lue
- `novelty-report` $0.025, événement primaire, 1× par run réussi

Run minimal (3 lignes) ≈ **$0.0252**. Run typique (2 000 lignes) ≈ **$0.12505**.

### Fichiers utiles

- `docs/DECISIONS.md` — ce qui est figé, et pourquoi CHANGED n’est pas implémenté
- `store/WORDING.md` — texte Store vs dedup / filtre / scraper
- `store/PUBLISH_CHECKLIST.md`
- `store/tracking/gates.json` — J0 / J7 / J30 / J60, seuils du gate, J0 encore vide
- `store/examples/` — inputs et forme du rapport

# Actor input Schema

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

Dataset ID of the current scrape (run N), for example the output of a Maps or leads Actor. Not a Google Maps URL.

## `items` (type: `array`):

Optional rows instead of datasetId. Use datasetId for real scrapes. Do not set both.

## `historyDatasetId` (type: `string`):

Your dataset of identity records from earlier runs. Leave empty on the first run to seed the named key-value store maps-leads-novelty-history. Do not set this together with a key-value store.

## `historyKeyValueStoreId` (type: `string`):

Your key-value store that holds the history document. The store must already exist. A missing key inside an existing store is treated as an empty seed.

## `historyKey` (type: `string`):

Key inside historyKeyValueStoreId. Default NOVELTY_HISTORY. Ignored unless a key-value store id is set.

## `identityMode` (type: `string`):

placeId, email, or auto (placeId if present, otherwise email, otherwise a less reliable name+address hash or E.164 phone).

## `emitMode` (type: `string`):

new_only emits unseen rows after the seed run. seed_history_only updates history and emits nothing. new_and_changed is deferred and fails the run.

## `failIfNoveltyRateBelow` (type: `number`):

Fail the run when new / (new + known) is strictly below this threshold. Ignored on the seed run. Example: 0.2 fails when fewer than 20% of classified identities are new.

## `updateHistory` (type: `boolean`):

Append NEW identities to your history after a successful run. A failed novelty gate does not write history.

## `normalizeEmail` (type: `boolean`):

Compare emails after lowercasing, trimming, and stripping %20 and spaces.

## Actor input object example

```json
{
  "historyKey": "NOVELTY_HISTORY",
  "identityMode": "auto",
  "emitMode": "new_only",
  "updateHistory": true,
  "normalizeEmail": true
}
```

# Actor output Schema

## `newItems` (type: `string`):

Rows classified NEW. Empty on the seed run, on seed_history_only, and when the novelty gate fails.

## `noveltyReport` (type: `string`):

Counts for this run: itemsScanned, newCount, knownCount, noveltyRate, firstRun, historyUpdated.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("fg-ei/maps-leads-novelty-gate").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("fg-ei/maps-leads-novelty-gate").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 '{}' |
apify call fg-ei/maps-leads-novelty-gate --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,fg-ei/maps-leads-novelty-gate"
        }
    }
}
```

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/XVa2T7Od4mWjaQomx/builds/QzXupXYc4oSR2gvek/openapi.json
