# Grounded Answer Door — No AI, Every Sentence Quoted or Refused (`equ1n0x/grounded-answer-door`) Actor

Hand it what a model wrote plus the evidence you gathered. It returns the sentences your evidence actually carries, each with the exact quote — and the ones it doesn't, each with the words that appear nowhere in your evidence. No AI, nothing remembered between runs.

- **URL**: https://apify.com/equ1n0x/grounded-answer-door.md
- **Developed by:** [Noah Davidson](https://apify.com/equ1n0x) (community)
- **Categories:** AI, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $100.00 / 1,000 claim checkeds

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/platform/actors/running/actors-in-store#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

## Grounded Answer Door — catch the word your source doesn't contain, down to the digit

**Your model wrote a paragraph. Somewhere in it is a word your source never contained — a changed
number, an added name. This finds it and names it, with the quote (or the gap) as the receipt. Here
it is on three sentences: one your source carries word for word, one changes a single digit, one
shares no words with it at all.**

```text
PASS  [verbatim]  Start on the free plan, no credit card required.
                  ← "Start on the free plan, no credit card required."   (example.com/pricing)

HELD              Our support team replies within 12 hours.
                  words not in any source: ["12"]        ← your source said 24

HELD              Refunds are instant.
                  words not in any source: ["refunds", "are", "instant"]
```

One digit changed, and it named the digit. It never decides whether a sentence is *true* — it can't,
and doesn't pretend to; it reports which of your words your source contains and which it doesn't, the
same way every time, nothing remembered between runs. *(A faithful paraphrase that swaps in a word
your source didn't use is named too — it checks words, not meaning, and leaves "is that okay?" to you.)*

**In plain words:** give it two pieces of text — some writing, and the source it's supposed to come
from. It goes **sentence by sentence** and tells you which ones your source actually carries (quoting
the exact words) and which it doesn't (naming the words that appear nowhere in it). It doesn't care
what wrote the text — a model, a contractor, you last year.

***

### The one call it refuses to make — and why that's the whole point

Every "hallucination detector" answers a question no machine can honestly answer: *does this sentence
mean what the source means?* To answer it, they **interpret** — they decide two different wordings are
"the same" — and hand you a confidence score for a judgment that was never theirs to make.

This door refuses that. It works at the **token floor** — a token being any atom your text is made
of: a word, a name, a lone digit. Down there, one question *is* answerable without interpreting:
**is this token carried by your source?** A token your source doesn't have is a unit of meaning it
never carried — so the door names it, and stops. Whether that addition is acceptable is a
meaning-call, and the meaning-call is **yours**.

That refusal isn't a limitation — it *is* the feature. You get the complete, addressed map of
everywhere your text left your source, and never a machine pretending to know the one thing it can't.
This is grounding done at the only floor honest enough to stand on. It is not the weaker cousin of
the "semantic" checkers — it draws the line where a machine can stay honest: it *will* tell you a word
entered that your source never had (a difference it can actually see), and it will *never* tell you
whether a rewording "means the same" (a judgement it can't). The "semantic" tools guess the second and
skip the first.

***

### What it's used for

| You want to… | Do this |
|---|---|
| Stop a RAG or agent answer from shipping claims your sources never made | `fail-closed` mode as a gate before the answer goes out |
| Show a client exactly which sentences of an AI-written report rest on which source | `annotate` mode; hand them the rows |
| Measure how much of a model's output its own sources actually contain, across versions or prompts | `annotate`, and count `source_carries_the_words` |
| Find the *one* word inside an otherwise-carried paragraph that your source never had | read `words_not_in_any_evidence` |
| Prove an answer was checked, to someone who wasn't there | set a seal secret and share it |

**What it does not do:**

- **A fact checker.** It reports whether *your evidence* carries a sentence's words. Whether your
  evidence is *right* is a different question, and this door has no opinion about it.
- **A rewriter.** It never edits, softens, or repairs a sentence. It reports; you decide.
- **A judge of meaning.** It does not decide whether a paraphrase "means the same thing" — deciding
  that is the interpreting it refuses to do. The practical cost, said plainly: a faithful paraphrase
  that swaps in a synonym your source didn't use is **held**, named by the new word. It checks words,
  not meaning; the "is that okay?" is yours.

***

### Quick start

1. **Press Start with the defaults.** The prefilled input is the example above: three sentences and
   two pieces of evidence. Run it as-is and watch one pass and two be held.
2. **Open the Dataset tab** — one row per sentence, `source_carries_the_words: true` or `false`.
3. **Read `words_not_in_any_evidence` on a held row.** It names which words are in your writing and
   in none of your source.
4. **Now use your own.** Paste your model's answer into *What the model wrote*, and either paste
   evidence inline or put a dataset ID into *Evidence dataset ID*.
5. **Switch to `fail-closed`** when you want it as a gate rather than a report.

***

### How a sentence is carried

Two ways, both exact, neither a score.

- **verbatim** — your evidence contains the sentence. Nothing to dispute.
- **carried** — every word of the sentence appears somewhere in the evidence. The wording may move;
  it may not introduce a word the evidence lacks. **A sentence that adds a word adds a claim**, and
  the added word is exactly where it left the ground.

**Every word counts — there is no stopword list and no minimum length**, and that is deliberate. An
earlier version ignored short words as "not content", and `12 hours` sailed through evidence saying
`24 hours`, because `12` is two characters. There is no honest length to set here: deciding which of
your words matter is deciding what your sentence means. So none are excluded.

The cost is that the door is strict — a rephrasing that adds a stray `to` is held. That is
harmless, because **every refusal carries its address**: `missing: ["to"]` and `missing: ["12"]` are
told apart at a glance, and neither was decided for you.

Case, curly quotes and spacing are folded, because those are copy-paste differences. **Word order,
negation, and numbers are never folded** — folding those would be interpreting.

#### What `carried` means, exactly

Carriage asks whether **one quote** holds every word of the sentence. Words can never be assembled
from two different sources. Within a single quote, though, they may be **re-arranged**:

```text
evidence:  "Free plan users get email support, while paid users get phone support."
answer:    "Free plan users get phone support."          →  carried
```

Every word is there, in a different arrangement. The result is not a claim that the sentence is
true, and not a claim that it is false: that evidence neither says free users get phone support
nor says they don't. The sentence is *unaddressed*. This reports carriage, not truth.

When a match is `carried` and not `verbatim`, the row says so directly:

```jsonc
"not_stated_by_the_evidence":
  "Every word is in the evidence, but the evidence does not state this. Whether it is
   so is unaddressed — if it matters, it is a question to go and answer, not an error
   to correct."
```

*Does the free plan include phone support?* is a question the source never answered, sitting
inside a sentence whose words were all present.

So the two relations are both honest, and every row tells you which one you got:

- **`verbatim`** — the sentence occurs in the evidence. Nothing has been re-arranged.
- **`carried`** — every word occurs in one quote, possibly ordered differently.

A reader can see the difference at a glance. **A gate cannot** — fail-closed decides for you. That is
what **Require word-for-word matches** is for: as a shipping gate, you may want the relation that
leaves nothing to re-arrange.

#### Every quote is a selection — so you always get the surroundings

A sentence is usually a *part* of a longer piece of source text, and the words around it can be
doing real work:

```text
source:  "It is false that free users get phone support."
sentence:"Free users get phone support."     →  verbatim
                                                before: "it is false that"

source:  "Our support team replies within 24 hours on business days."
sentence:"Support replies within 24 hours."  →  carried
                                                source also says: [our, team, on, business, days]
```

The first is quote-mining, the second ordinary quoting. To a machine they are identical — both a
sentence sitting inside a longer one — and telling them apart means reading what the surrounding
words do.

So there is no flag. Every match hands back what surrounds it, unlabelled.

Fail-closed guarantees no sentence ships carrying words your source does not have. It does not
guarantee a shipped sentence represents your source faithfully — that requires a reader.

***

### The question check

Evidence gathered to answer one question is not evidence for a different one.

If your evidence records the question it was gathered under — everything from the
**[Evidence-Quoted Page Classifier](https://apify.com/equ1n0x/seo-marketing-framing-analyzer)**
does, on every row — the report tells you whether that matches the question you gave here.

It is **reported, never enforced.** Grounding an answer in neighbouring evidence is a legitimate
thing to do and it is your call. Being told is not optional, because this failure is invisible
otherwise: every quote real, every citation checkable, and the entire answer resting on evidence
collected to answer something else.

***

### The evidence item — the one shape this Actor agrees on

The **screen** speaks plainly (*your source text*, *the writing you want checked*). The **fields**
have fixed names, because they are how this Actor and whatever feeds it agree on what a piece of
source text is. Stated here rather than assumed:

| key | | |
|---|---|---|
| `evidence_quote` *(or `text`)* | **required** | a span of the source, exactly as it appears |
| `source_url` *(or `url`)* | optional | where it came from |
| `question` | optional | what it was gathered to answer — enables the check below |
| `lens` | optional | which label set produced it |

Anything with those keys works. Rows from the
[Evidence-Quoted Page Classifier](https://apify.com/equ1n0x/seo-marketing-framing-analyzer) match by
construction, which is why chaining the two needs no glue.

**If your items don't match, you'll be told exactly that.** A dataset whose fields are named
differently produces items that cannot be read — and "I was given nothing" and "I was given things I
couldn't read" are different answers, so this Actor gives you the second one, with the count and the
key it was looking for. Guessing that you'd sent nothing would have been the easier message and the
wrong one.

So: plain words where a person reads, stable names where a machine does, and the shape written down
where both can check it.

***

### What comes back

**One row per sentence** — `claim`, `source_carries_the_words`, `carried_by` (how, the quote, the source) or
`words_not_in_any_evidence`, the `question`, and a tamper-evident `seal`.

**`GROUNDING_REPORT`** in the key-value store — counts, every uncarried sentence with its address, the
question check, and its own seal. Hand it to whoever receives the answer.

Set a **seal secret** and share it with whoever receives the result: they can verify the rows and the
report weren't altered in transit **without you present**.

#### Verifying a seal — and exactly what it proves

Every row and the report carry a `seal`. Recompute it and compare — no tool of ours required:

```python
import hashlib, hmac, json

def expected_seal(record: dict, secret: str | None) -> str:
    body = {k: v for k, v in record.items() if k != "seal"}
    payload = json.dumps(body, sort_keys=True, ensure_ascii=False, default=str).encode()
    if secret:  # grd1: keyed
        key = hashlib.pbkdf2_hmac("sha256", secret.encode(), b"equinox-grounding-v1", 200_000, 32)
        return "grd1:" + hmac.new(key, payload, hashlib.sha256).hexdigest()[:32]
    return "grd0:" + hashlib.sha256(payload).hexdigest()[:32]  # grd0: public checksum

assert record["seal"] == expected_seal(record, my_secret_or_None)
```

**What each tag actually protects — stated plainly, because a seal you misread is worse than none:**

- **`grd0:` (no secret)** is a **public checksum.** It catches accidental change and casual editing —
  but anyone who alters a row can recompute `grd0:` too. It proves integrity in good faith, **not**
  against a motivated forger.
- **`grd1:` (seal secret set)** is an **HMAC.** Nobody *without* the secret can alter a row
  undetected. But it is **symmetric**: whoever holds the secret to verify can also produce valid
  seals. So `grd1:` proves *"sealed by a holder of this secret"* — it detects third-party tampering in
  transit, but it is **not a public signature** (that would need asymmetric keys). Give each recipient
  their own secret, and don't publish it. That is exactly as much as it claims, and no more.

***

#### Why the field isn't called `grounded`

Because this Actor doesn't know that. It knows **your source contains this sentence's words** —
`source_carries_the_words` — and that is a fact it can check. *Grounded* would mean supported, and
support is a judgement about what the surrounding words do, which is reading.

It matters most exactly where it's least visible. These two rows were identical under the old name:

```text
source: "It is false that free users get phone support."   →  true
source: "Refunds are available."                           →  true
```

One is mined out of a denial. A person reading `carried_by` sees `"it is false that"` immediately; a
pipeline reading one boolean sees neither. So the boolean says only what is true of it, and
everything that distinguishes those two rows sits in `carried_by`, where it belongs.

***

### Two modes

| Mode | What happens |
|---|---|
| **Annotate** | Every sentence comes back, marked, with its evidence or its address. A report. |
| **Fail-closed** | If even one sentence is not carried, **nothing is emitted** and the run fails. A gate: your answer doesn't ship until it's grounded or edited. |

***

### Pricing

Two meters. One for the reading, one for the catch — and they are different things, so they are priced differently.

| What comes home | The counter | Price |
|---|---|---|
| Each sentence checked against your source, with the exact quote that carries it | One check for every sentence | **$0.10 / sentence** ($100 / 1,000) |
| Where it fails, the unsupported words themselves — the ones that appear nowhere in your evidence | One additional catch, only where your source does not carry it | **$1.00** each |

**Why the catch costs more than the check.** A sentence that reads clean is work done. A sentence your source doesn't carry is a **false claim that did not ship** — a changed number, an added name, a citation to something that isn't there — caught before it reached whoever was going to read it. You are not paying more for a worse result; you are paying for the thing that didn't happen, at something nearer what it would have cost.

**Silence is free, and so is the refusal.** There is no per-run fee. This boots a standard-library process — no browser, nothing to amortise — so a startup charge would be a number nothing derived. And the two ways this Actor declines to work are free: a missing `answer`, and evidence with no readable text. Both fail on purpose, because grading against nothing would mark every sentence unsupported and look like a finding. Charging for that would bill you for the moment it was most careful.

A worked run: a 40-sentence draft against a real source, 6 sentences not carried — $4.00 for the reading, $6.00 for the six catches, **$10.00**, and six things you would otherwise have published.

#### How this price was derived

Priced by derivation, not by market-positioning. Professional fact-checking runs about $50/hour, which lands near $1–5 per claim; this reads every sentence for a tenth of the bottom of that, and only charges the higher rate where something was actually caught. The comparable tools sell a monthly subscription and return a **confidence score** — a probability that a sentence might be unsupported. This returns the words that are absent, and bills only when there are some.

**Read that $1–5 as a ceiling, not as the basis** — the distinction matters and it is deliberate.
What a checker is *paid* is not what sets this price, because pricing a tool at what a person earns
makes that person's wage into the seller's margin, and we will not do that. What sets the price is
**measured cost plus a stewardship wage that recovers the time already spent building the engine** —
a finite number, already spent. So it is a debt rather than a rent: as it is recovered the price
**slackens toward the free margin**, because a debt collected twice is a rent. The gap between the
ten cents you pay and what the same read is worth to you is not margin forgone. It is yours.

***

### Pairs with

```text
   your question
        │
        ▼
  Evidence-Quoted Page Classifier  ──►  evidence, quoted, carrying its question
        │
   your model ──► its answer ──►  THIS DOOR  ──►  what the evidence carries
        │                              │
        │                              └──►  what it doesn't, and where it ran out
        ▼
  Dataset Egress Firewall  ──►  only the fields you named, sealed  ──►  out
```

The [classifier](https://apify.com/equ1n0x/seo-marketing-framing-analyzer) gathers evidence that
carries its own question. This door checks what a model says against it. The
[firewall](https://apify.com/equ1n0x/dataset-egress-firewall) governs which fields leave. Same
terms throughout: deterministic, no AI, receipts at every step.

***

### Servicing of terms

Not terms that govern the service — a service that keeps its terms. **Your text stays yours**
(processed in-container, never retained after the run, never sold, never trained on, never sent to
any model — the only network it does at all is reading the evidence dataset you point it at, through
Apify's own API; nothing goes to any external service). **Nothing is remembered between runs**, by
construction rather than by policy: a door with a memory could ground a sentence in something it did
not read. **No rights are claimed** over your inputs or outputs. **You can leave anytime with
nothing held.** One ask back: use it to keep answers honest — not to make a source that doesn't hold
a claim look like it does.

# Actor input Schema

## `answer` (type: `string`):

Paste it in. It's read sentence by sentence, split on . ! ? — so an abbreviation like "e.g." or "Inc." can split unexpectedly. It's never re-worded, and it doesn't matter what wrote it.

## `question` (type: `string`):

Optional, in your own words. Used to check that your source text was actually gathered to answer this — source material collected for a different question isn't support for this one.

## `evidence` (type: `array`):

Each item needs an `evidence_quote` — a piece of the source, exactly as it appears — and optionally a `source_url`. Use this, or point at a dataset below, or both.

## `evidenceDatasetId` (type: `string`):

A dataset from the Evidence-Quoted Page Classifier, or any Actor emitting `evidence_quote`. Its quotes become your source text, and its question is read for the check above.

## `mode` (type: `string`):

Annotate: everything comes back, carried or not, each with its evidence or the words that are missing. Fail-closed: if even one sentence is not carried, nothing is emitted and the run fails — use it as a shipping gate.

## `verbatimOnly` (type: `boolean`):

Off by default. Off, a sentence passes when one quote contains all of its words in any order — which can be recombined into something the quote doesn't say. On, the sentence must appear inside the evidence exactly, and there is nothing left to recombine. Turn it on when you're using fail-closed as a gate, because a gate can't inspect rows the way you can.

## `maxEvidence` (type: `integer`):

How many pieces of evidence to read from the dataset.

## `sealSecret` (type: `string`):

Share it with whoever receives the result and they can check the rows and the report are unaltered — without you present.

## Actor input object example

```json
{
  "answer": "Start on the free plan, no credit card required. Our support team replies within 12 hours. Refunds are instant.",
  "question": "What does the free plan include?",
  "evidence": [
    {
      "evidence_quote": "Start on the free plan, no credit card required.",
      "source_url": "https://example.com/pricing"
    },
    {
      "evidence_quote": "Our support team replies within 24 hours on business days.",
      "source_url": "https://example.com/support"
    }
  ],
  "mode": "annotate",
  "verbatimOnly": false,
  "maxEvidence": 1000
}
```

# 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 = {
    "answer": "Start on the free plan, no credit card required. Our support team replies within 12 hours. Refunds are instant.",
    "question": "What does the free plan include?",
    "evidence": [
        {
            "evidence_quote": "Start on the free plan, no credit card required.",
            "source_url": "https://example.com/pricing"
        },
        {
            "evidence_quote": "Our support team replies within 24 hours on business days.",
            "source_url": "https://example.com/support"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("equ1n0x/grounded-answer-door").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 = {
    "answer": "Start on the free plan, no credit card required. Our support team replies within 12 hours. Refunds are instant.",
    "question": "What does the free plan include?",
    "evidence": [
        {
            "evidence_quote": "Start on the free plan, no credit card required.",
            "source_url": "https://example.com/pricing",
        },
        {
            "evidence_quote": "Our support team replies within 24 hours on business days.",
            "source_url": "https://example.com/support",
        },
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("equ1n0x/grounded-answer-door").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 '{
  "answer": "Start on the free plan, no credit card required. Our support team replies within 12 hours. Refunds are instant.",
  "question": "What does the free plan include?",
  "evidence": [
    {
      "evidence_quote": "Start on the free plan, no credit card required.",
      "source_url": "https://example.com/pricing"
    },
    {
      "evidence_quote": "Our support team replies within 24 hours on business days.",
      "source_url": "https://example.com/support"
    }
  ]
}' |
apify call equ1n0x/grounded-answer-door --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,equ1n0x/grounded-answer-door"
        }
    }
}

```

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/Ml407r4h5LHvKahTG/builds/CcRuK5kHF28lymp4g/openapi.json
