# Agent Receipt Verifier (`honestagentkit/agent-receipt-verifier`) Actor

Check what an AI agent says it did. Give file claims (expected sha256) or URL claims (expected HTTP status). Each claim gets PROVEN, REJECTED or UNVERIFIABLE with evidence hashes, plus a sha256 run receipt.

- **URL**: https://apify.com/honestagentkit/agent-receipt-verifier.md
- **Developed by:** [Honest Agent Kit](https://apify.com/honestagentkit) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 claim verifieds

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

## Agent Receipt Verifier

Check what an AI agent says it did, against real bytes and real HTTP answers.

Your agent reports "I wrote report.md, its sha256 is 3f7c…" or "the page is live, it returns 200". This Actor checks each claim and gives it one of three verdicts, with the evidence and its hash. At the end it writes a run receipt: the sha256 of all results in canonical JSON, so anyone can recompute it later.

### Verdicts

| Verdict | Meaning |
| --- | --- |
| **PROVEN** | The actual bytes were hashed, or the actual HTTP status was read, and they match the claim. |
| **REJECTED** | The actual bytes or status were checked, and they do **not** match. A real mismatch, never a guess. |
| **UNVERIFIABLE** | The evidence could not be obtained, or the answer does not settle the claim. Timeouts, DNS failures, oversize files, blocked addresses, unfollowed redirects, missing or bad input, and local paths all land here, never in REJECTED. |

A claim is never PROVEN unless the bytes or the status were actually checked in this run.

### What you can check

#### File claims (expected sha256)

A cloud run cannot see your laptop's disk, so give the bytes in one of three ways:

| Field | How the bytes are obtained |
| --- | --- |
| `fileUrl` | One `GET` to an http(s) URL you supply. The body is streamed into sha256, up to **Max file size** (default 10 MB). `Accept-Encoding: identity` is requested; a compressed answer is UNVERIFIABLE because its hash would not be the file's. |
| `contentBase64` | The file bytes inline, as standard base64. |
| `kvKey` | A record in an Apify key-value store you choose in **File store**. The record's raw bytes are hashed. |
| `path` | Accepted so agent output can be passed straight in, but **never read**. Always UNVERIFIABLE with the reason "local paths cannot be read from the Apify cloud". |

#### URL claims (expected HTTP status)

One request to the URL you supply (`GET` by default, or `HEAD`). Only the status line and headers are read; the connection is closed before the body. Redirects are **not** followed:

- If the claim expects a redirect (for example `301`) and gets it, that is PROVEN.
- If the claim expects `200` and the server answers `302`, the claim is not settled: UNVERIFIABLE. Give the final URL instead.
- `404` claimed and `404` answered is PROVEN, which is how you check "I deleted that page".
- `401`, `403`, `405`, `407`, `408`, `425`, `429` and `5xx` answers that differ from the claim are UNVERIFIABLE: the server answered but would not confirm (it may need a login, block automated requests, rate limit, or be down).
- Any other different status is REJECTED.

### Input example

```json
{
  "claims": [
    { "id": "homepage-live", "type": "url", "url": "https://example.com/", "expectedStatus": 200 },
    { "id": "old-page-removed", "type": "url", "url": "https://example.com/old-page", "expectedStatus": 404 },
    { "id": "release-notes", "type": "file", "fileUrl": "https://example.com/files/notes.txt", "expectedSha256": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03" },
    { "id": "inline-hello", "type": "file", "contentBase64": "aGVsbG8K", "expectedSha256": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03" },
    { "id": "stored-output", "type": "file", "kvKey": "agent-output", "expectedSha256": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03" },
    { "id": "laptop-file", "type": "file", "path": "C:/agent/out/report.md", "expectedSha256": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03" }
  ],
  "fileStore": "<your key-value store ID>",
  "maxClaims": 100,
  "timeoutSecs": 15,
  "maxFileBytes": 10485760
}
```

| Field | Default | Notes |
| --- | --- | --- |
| `claims` | required | 1 to 500 claims. Each needs a unique `id` and a `type` (`file` or `url`). |
| `fileStore` | this run's default store | Key-value store that holds `kvKey` records: pick it in the input form, or pass its ID (or `username~store-name`) through the API. Read-only access is requested. |
| `maxClaims` | 100 | Claims past this are returned UNVERIFIABLE ("not checked") and are not charged. Max 500. |
| `timeoutSecs` | 15 | Per request, including the download. 1 to 60. |
| `maxFileBytes` | 10 MB | Larger files are UNVERIFIABLE (not hashed). Max 50 MB. |
| `maxConcurrency` | 4 | Claims checked at the same time. 1 to 10. |

### Output

One dataset item per claim, in input order (`index`). Example (illustrative values):

```json
{
  "index": 0,
  "claimId": "homepage-live",
  "claimType": "url",
  "target": "https://example.com/",
  "verdict": "PROVEN",
  "code": "MATCH",
  "reason": "server answered 200, as claimed",
  "expected": 200,
  "actual": 200,
  "checkedAt": "2026-10-04T01:54:54.640Z",
  "evidence": { "kind": "http-status", "method": "GET", "url": "https://example.com/", "httpStatus": 200, "remoteAddress": "93.184.215.14" },
  "evidenceSha256": "9c1d…",
  "billable": true
}
```

- `expected` / `actual`: the sha256 (file) or the HTTP status (url). `actual` is `null` when nothing was observed.
- `evidence`: what was observed (status, address, byte count, content sha256, error code). `null` when nothing was fetched or read.
- `evidenceSha256`: sha256 of the canonical JSON of `evidence`.
- `code`: machine-readable reason: `MATCH`, `SHA256_MISMATCH`, `STATUS_MISMATCH`, `TIMEOUT`, `DNS_FAILURE`, `CONNECTION_FAILED`, `TLS_ERROR`, `OVERSIZE`, `ENCODED_BODY`, `REDIRECT_NOT_FOLLOWED`, `HTTP_NOT_OK`, `SERVER_WOULD_NOT_CONFIRM`, `BLOCKED_ADDRESS`, `LOCAL_PATH`, `MISSING_INPUT`, `BAD_INPUT`, `KV_RECORD_NOT_FOUND`, `KV_READ_FAILED`, `OVER_CLAIM_CAP`, `BUDGET_REACHED`.
- `billable`: `true` when the claim was actually checked and counted as one `claim-verified` event.

The default key-value store also gets:

| Key | Content |
| --- | --- |
| `SUMMARY` | Counts per verdict, checked / not checked, charged events, limits used, start and finish times, and the receipt hash. |
| `RECEIPT` | `resultsSha256`, the algorithm, the canonicalization rule, and the verdict and evidence hash of every claim. |
| `RESULTS_CANONICAL` | The exact bytes that were hashed for the receipt. |

#### Recompute the receipt

The receipt is `sha256` of the results, ordered by `index`, as JSON with object keys sorted and no whitespace (UTF-8). Download `RESULTS_CANONICAL` and hash it, or rebuild it from the dataset:

```python
import hashlib, json
items = sorted(json.load(open("dataset.json")), key=lambda x: x["index"])
canon = json.dumps(items, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
print(hashlib.sha256(canon).hexdigest())  # equals RECEIPT.resultsSha256
```

### How it fetches (and what it never does)

- It requests **only the URLs you supply**, **one request per claim**, with a polite `User-Agent` (`AgentReceiptVerifier/1.0 …`), a timeout and a size cap.
- It does not crawl, follow links, follow redirects, retry, parse pages, run scripts or use proxies.
- **Address guard:** before connecting it resolves the host and refuses private, loopback, link-local, carrier-grade NAT, multicast, reserved and cloud-metadata addresses (for example `10.0.0.0/8`, `127.0.0.0/8`, `169.254.169.254`, `fd00::/8`), including IPv4-mapped IPv6 forms and names that resolve there. The checked address is the one connected to. Blocked claims are UNVERIFIABLE and not charged.
- URLs with embedded user names or passwords, and schemes other than http and https, are refused.

### Honest limits

- **It checks only what it can fetch.** A PROVEN file claim means the bytes at that URL, in that inline string or in that record hashed to the expected value at `checkedAt`. It does not prove the file on the agent's own disk is the same, or who made it.
- It does not check that the content is correct, only that the hash matches.
- A PROVEN URL claim means the server gave that status to this one request. Some sites answer `200` for any path, and some answer differently to datacenter IPs, so a `200` does not prove the page says what the agent claimed.
- Many sites block or challenge automated requests from cloud IPs. Those answers come back UNVERIFIABLE, not REJECTED.
- No time-window check: unlike a local disk checker, a cloud run cannot see when a file was last modified, so "created at 14:02" is not checked.
- Results describe one moment. Run it again for a fresh check.

### Pricing

Pay per event:

- **$0.0015 per claim verified** (`claim-verified`, $1.50 per 1,000 claims). Charged once for each claim that was actually checked: bytes downloaded, decoded or read, or one HTTP request made, whatever the verdict. A timeout or DNS failure counts, because the request was made.
- **Not charged:** claims with missing or bad input, local paths, blocked addresses, claims past `maxClaims`, and claims skipped because your spending limit was reached.
- A small Actor start fee set by Apify (`apify-actor-start`, $0.00005 per start at up to 1 GB memory).

Each dataset item says whether it was charged (`billable`), and `SUMMARY.chargedEvents` gives the total. If you set a maximum cost per run, the Actor stops checking when it is reached and returns the rest as UNVERIFIABLE ("not checked").

### Use from an agent or pipeline

Call it with the Apify API, the Apify MCP server, or an integration, pass the claims your agent produced, then read the dataset or the `RECEIPT` record. Gate the next step on `verdict === "PROVEN"`, and treat UNVERIFIABLE as "a person should look".

### Changelog

See [CHANGELOG.md](CHANGELOG.md).

### Author and disclosure

Built by a retired military veteran, with AI coding assistance. Tested by the seller with unit tests and local runs against throwaway test servers, as listed in the changelog. MIT license.

# Changelog

This Actor's version history is a separate document: https://apify.com/honestagentkit/agent-receipt-verifier/changelog.md

# Actor input Schema

## `claims` (type: `array`):

One object per claim. Every claim needs an id and a type ("file" or "url").

File claim: {"id", "type": "file", "expectedSha256", and exactly one of "fileUrl" (http/https), "contentBase64" or "kvKey" (a record in the File store below)}. A bare local "path" is accepted but always comes back UNVERIFIABLE, because a cloud run cannot read your disk.

URL claim: {"id", "type": "url", "url", "expectedStatus", optional "method": "GET" or "HEAD" (default GET)}.

## `fileStore` (type: `string`):

Key-value store that holds the records named by kvKey in file claims. Pick it here, or pass its ID (or username~store-name) through the API. Leave empty to use this run's default store. Read-only access is requested.

## `maxClaims` (type: `integer`):

Claims after this many are returned as UNVERIFIABLE (not checked) and are not charged.

## `timeoutSecs` (type: `integer`):

Seconds allowed for one request, including the download. A timeout is UNVERIFIABLE.

## `maxFileBytes` (type: `integer`):

Largest file that is hashed. Bigger files are UNVERIFIABLE (not hashed). Default 10 MB, maximum 50 MB.

## `maxConcurrency` (type: `integer`):

How many claims are checked at the same time.

## Actor input object example

```json
{
  "claims": [
    {
      "id": "homepage-status",
      "type": "url",
      "url": "https://example.com/",
      "expectedStatus": 200
    },
    {
      "id": "inline-hello",
      "type": "file",
      "contentBase64": "aGVsbG8K",
      "expectedSha256": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"
    },
    {
      "id": "laptop-file",
      "type": "file",
      "path": "C:/agent/out/report.md",
      "expectedSha256": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"
    }
  ],
  "maxClaims": 100,
  "timeoutSecs": 15,
  "maxFileBytes": 10485760,
  "maxConcurrency": 4
}
```

# Actor output Schema

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

One row per claim: PROVEN, REJECTED or UNVERIFIABLE, with evidence.

## `summary` (type: `string`):

No description

## `receipt` (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 = {
    "claims": [
        {
            "id": "homepage-status",
            "type": "url",
            "url": "https://example.com/",
            "expectedStatus": 200
        },
        {
            "id": "inline-hello",
            "type": "file",
            "contentBase64": "aGVsbG8K",
            "expectedSha256": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"
        },
        {
            "id": "laptop-file",
            "type": "file",
            "path": "C:/agent/out/report.md",
            "expectedSha256": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("honestagentkit/agent-receipt-verifier").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 = { "claims": [
        {
            "id": "homepage-status",
            "type": "url",
            "url": "https://example.com/",
            "expectedStatus": 200,
        },
        {
            "id": "inline-hello",
            "type": "file",
            "contentBase64": "aGVsbG8K",
            "expectedSha256": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03",
        },
        {
            "id": "laptop-file",
            "type": "file",
            "path": "C:/agent/out/report.md",
            "expectedSha256": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("honestagentkit/agent-receipt-verifier").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 '{
  "claims": [
    {
      "id": "homepage-status",
      "type": "url",
      "url": "https://example.com/",
      "expectedStatus": 200
    },
    {
      "id": "inline-hello",
      "type": "file",
      "contentBase64": "aGVsbG8K",
      "expectedSha256": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"
    },
    {
      "id": "laptop-file",
      "type": "file",
      "path": "C:/agent/out/report.md",
      "expectedSha256": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"
    }
  ]
}' |
apify call honestagentkit/agent-receipt-verifier --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,honestagentkit/agent-receipt-verifier"
        }
    }
}
```

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/7ita17GwCX9WOaJRR/builds/oPiJ2ewjhY0zeejQs/openapi.json
