# AI Answer Visibility Check: five rules and a five-check script (free sample)

Version 1.0, 2026-10-29. By PaperChaseWebb. For anyone who wants to know whether ChatGPT, Claude, Gemini, Perplexity or Grok names their business when a buyer asks. One Markdown file and one Python 3 script, standard library only. You ask the assistants yourself and save the answers; the script reads them. It opens no network connection and never asks an assistant anything. It was tested on its worked example (5 findings, then 0) and on a missing folder (exit 2). It was not run inside any agent harness or against any real assistant, and the example business and answers are made up.

One answer from an assistant is not a pattern. Ask the same buyer question in a new chat and you may get a different list of names. This sample is built around that: three fresh chats per question before anything counts as a gap, your name kept out of the question, and the answer saved exactly as it came.

## Install in one line

Save this page as FREE-SAMPLE.md, then:

```
python3 -c "import re;t=open('FREE-SAMPLE.md').read();open('vis_mini.py','w').write(re.search(r'~~~python\n(.*?)~~~',t,re.S).group(1))"
python3 vis_mini.py --example demo
python3 vis_mini.py demo/before
```

## Five rules

1. **Leave your name out of the question.** Ask what a buyer types before they know you: "bookkeeper for a food truck in Portland", not "is Harbor Lane good".
2. **Fresh chat, every time.** A new chat per run, memory off where you can. Otherwise the second answer just repeats the first.
3. **Three runs before you call it.** Named in none of three is a gap. Named in one or two is mixed, and usually the easiest to fix.
4. **Save the answer as it came, with the date.** No tidying. The links an assistant cites are the most useful part: they are the pages it leaned on.
5. **Change your own pages, with your own yes.** Read the cited sources, then change what your site says. Do not buy listings, post reviews or contact the sites. Ask by hand or with your own official API key; do not automate a chat window.

## The five checks

| Id | Check |
|---|---|
| M1 | a question names your brand |
| M2 | an asked_at date is not YYYY-MM-DD, or is after as_of |
| M3 | an answer file is missing |
| M4 | named_us does not match the saved answer |
| M5 | a question and assistant pair has fewer than 3 runs |

Your folder holds check.json (brand, as_of, questions), answers.csv (q_id, assistant, run, asked_at, file, named_us) and an answers/ folder of .txt files. Run --example to see one.

## The script

~~~python
#!/usr/bin/env python3
"""vis_mini.py: the free five-check version of the AI Answer Visibility Check.

  python3 vis_mini.py FOLDER          check a folder holding check.json, answers.csv and answers/
  python3 vis_mini.py --example DIR   write a made-up example: DIR/before (5 findings) and DIR/after (0)

M1 a question names your brand (ask the way a buyer would)
M2 an asked_at date is not YYYY-MM-DD, or is after as_of
M3 an answer file is missing
M4 named_us does not match the saved answer
M5 a question and assistant pair has fewer than 3 runs, so nothing can be called a gap yet

Exit 0 clean, 1 findings, 2 folder or file missing. Standard library only, no network, writes nothing when checking.
"""
import csv, datetime, json, os, re, sys


def named(text, brand):
    return "yes" if re.search(r"(?<![\w])" + re.escape(brand) + r"(?![\w])", text, re.I) else "no"


def date(s):
    try:
        return datetime.date.fromisoformat(s) if re.fullmatch(r"\d{4}-\d{2}-\d{2}", s or "") else None
    except ValueError:
        return None


def check(d):
    cfg = json.load(open(os.path.join(d, "check.json")))
    rows = list(csv.DictReader(open(os.path.join(d, "answers.csv"), newline="")))
    brand, asof, out = cfg["brand"], date(cfg.get("as_of", "")), []
    for q in cfg.get("questions", []):
        if named(q["text"], brand) == "yes":
            out.append(("M1", f"question {q['id']} names {brand}; ask without your name"))
    runs = {}
    for r in rows:
        dt = date(r.get("asked_at", ""))
        if dt is None or (asof and dt > asof):
            out.append(("M2", f"{r['q_id']} {r['assistant']} run {r['run']}: asked_at {r.get('asked_at')!r} is not a date on or before as_of"))
        p = os.path.join(d, r.get("file", ""))
        if not r.get("file") or not os.path.isfile(p):
            out.append(("M3", f"{r['q_id']} {r['assistant']} run {r['run']}: answer file {r.get('file')!r} is missing"))
        elif named(open(p, encoding="utf-8").read(), brand) != r.get("named_us"):
            out.append(("M4", f"{r['q_id']} {r['assistant']} run {r['run']}: named_us is {r.get('named_us')!r}, the saved answer says otherwise"))
        runs.setdefault((r["q_id"], r["assistant"]), set()).add(r["run"])
    for (q, a), s in sorted(runs.items()):
        if len(s) < 3:
            out.append(("M5", f"{q} on {a} has {len(s)} run(s); ask 3 times in fresh chats before calling it a gap"))
    return out


def example(root):
    q = [{"id": "Q1", "text": "Who is a good bookkeeper for a small cafe?"},
         {"id": "Q2", "text": "Which bookkeeper handles payroll for restaurants?"}]
    ans = [("Q1", "1", "2026-10-21", "Harbor Lane Bookkeeping works with cafes."),
           ("Q1", "2", "2026-10-22", "Try Northshore Ledger."),
           ("Q1", "3", "2026-10-24", "Harbor Lane Bookkeeping or Tallyworks."),
           ("Q2", "1", "2026-10-21", "Northshore Ledger does restaurant payroll."),
           ("Q2", "2", "2026-10-23", "Tallyworks bundles payroll."),
           ("Q2", "3", "2026-10-26", "Northshore Ledger, mostly.")]
    for name in ("before", "after"):
        d = os.path.join(root, name); os.makedirs(os.path.join(d, "answers"), exist_ok=True)
        qs = [dict(x) for x in q]
        rows = []
        for qi, run, dt, text in ans:
            f = f"answers/{qi}-{run}.txt"
            open(os.path.join(d, f), "w").write(text + "\n")
            rows.append({"q_id": qi, "assistant": "ChatGPT", "run": run, "asked_at": dt, "file": f, "named_us": named(text, "Harbor Lane Bookkeeping")})
        if name == "before":
            qs[1]["text"] = "Does Harbor Lane Bookkeeping handle payroll for restaurants?"   # M1
            rows[1]["asked_at"] = "2026-11-02"                                              # M2
            rows[2]["file"] = "answers/Q1-3-missing.txt"                                    # M3
            rows[3]["named_us"] = "yes"                                                     # M4
            rows.pop()                                                                      # M5
        json.dump({"brand": "Harbor Lane Bookkeeping", "as_of": "2026-10-29", "questions": qs,
                   "note": "Made-up example data. No assistant was asked anything to make it."}, open(os.path.join(d, "check.json"), "w"), indent=1)
        with open(os.path.join(d, "answers.csv"), "w", newline="") as fh:
            w = csv.DictWriter(fh, fieldnames=["q_id", "assistant", "run", "asked_at", "file", "named_us"]); w.writeheader(); w.writerows(rows)
    print(f"example written to {root}/before and {root}/after")


def main(argv):
    if len(argv) == 3 and argv[1] == "--example":
        example(argv[2]); return 0
    if len(argv) != 2:
        print(__doc__, file=sys.stderr); return 2
    d = argv[1]
    if not os.path.isfile(os.path.join(d, "check.json")) or not os.path.isfile(os.path.join(d, "answers.csv")):
        print(f"error: {d} needs check.json and answers.csv", file=sys.stderr); return 2
    out = check(d)
    for c, m in out:
        print(c, m)
    print(f"{len(out)} finding(s)" + (": " + ", ".join(sorted({c for c, _ in out})) if out else ""))
    return 1 if out else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))
~~~

## Worked example (made up)

```
python3 vis_mini.py demo/before
```

```
M1 question Q2 names Harbor Lane Bookkeeping; ask without your name
M2 Q1 ChatGPT run 2: asked_at '2026-11-02' is not a date on or before as_of
M3 Q1 ChatGPT run 3: answer file 'answers/Q1-3-missing.txt' is missing
M4 Q2 ChatGPT run 1: named_us is 'yes', the saved answer says otherwise
M5 Q2 on ChatGPT has 2 run(s); ask 3 times in fresh chats before calling it a gap
5 finding(s): M1, M2, M3, M4, M5
```

The fixes: take the brand out of Q2, correct the date on run 2 (it was after as_of), put the missing answer file back, set named_us to no on the Q2 answer that does not name the business, and ask Q2 a third time.

```
python3 vis_mini.py demo/after
```

```
0 finding(s)
```

## One prompt to try

Paste into your agent: "Help me write five buyer questions for my business that do not contain my name. I will ask each one three times in fresh chats and save the answers in answers/. Then fill in answers.csv, run python3 vis_mini.py on the folder, and tell me which questions named me, which did not, and who was named instead. Do not ask any assistant yourself and do not change any page."

## The full pack

The paid AI Answer Visibility Check (pp-ai-answer-visibility-check, $19) grows this into fifteen read-only checks: the own-brand rule, allowed methods only, hashed answers that cannot be quietly edited, stale answers flagged after 30 days, a session label per run, your position among the names, who was named instead, the sources each answer cited, verdicts worked out again from the saved text, and page changes that wait for the owner's yes, tied to a sha256 and typed at their own terminal. It comes with agent files for Claude Code, Codex, Hermes and Grok Bot, ten prompts and a worked example. If you would rather have it done with you, book a call from the home page at paperchasewebb.ai.

Made-up example; the business, competitors and answers are invented.

PaperChaseWebb 💸
