human.json

8 min read Original article ↗

The human.json Protocol

human.json is a lightweight protocol for humans to assert authorship of their site content and vouch for the humanity of others. It uses URL ownership as identity, and trust propagates through a crawlable web of vouches between sites.

  • Version: 0.1.1
  • Status: Draft
  • Last Updated: 2026-03-07
  • Author: Beto Dealmeida

How it works

One of the problems with the internet today is that a lot of the content is AI generated. There's no way to know for sure if a site is maintained by a real human or if it's just slop. The only way to know for sure is by trusting the authors, which usually takes time and requires developing a relationship with them through other channels, like email or social media. But what if we could leverage that trust by building a web of vouches between sites?

human.json is a protocol based on that trust. It works the following way:

  1. You declare yourself to be a human and your commitment to avoid AI-generated content by posting a human.json file somewhere on your site and linking to it with <link rel="human-json">. The presence of this file indicates that the content of your site was generated by you, not AI. It's fine to use AI tools that assist you with spellchecking, grammar, formatting, etc. Just make sure to be clear and transparent about how AI is used, and even consider publishing an /ai slashpage with your policy.
  2. In the same human.json file you also vouch for the humanity of other sites that you trust. This is optional, but it creates a network of trusted sites that allows people to discover other real humans by following that web of vouches.

Browser extensions simplify the process of managing trust and identifying if a site is maintained by a human (Note: extensions have been submitted and are waiting approval, for now you need to install them manually from source). For example, when you first visit the site of someone who has published a human.json file you see this:

Discovering a site with human.json

Once you've trusted a site:

A trusted seed site

If you visit a site that is vouched by someone you trust, you will see a green dot, indicating that they are human:

A site vouched by a trusted seed

The color will vary depending on how many hops are needed to reach them from someone you trust: green for 0-1 hops (you trust them or someone who vouches for them), yellow for 2 hops, orange for 3+ hops.

With the extension you can manage the sites you trust, as well as block some of the sites that show up in your discovery:

Managing trusted seeds and discovered sites

The Protocol

Discovery

A site advertises its human.json file by including a <link> tag in the HTML <head>:

<link rel="human-json" href="/path/to/human.json">

The href can be any URL, absolute or relative. The file can live anywhere: at the root, in a subdirectory, on a shared host under ~/username/, etc. There is no fixed path.

Verifiers discover the file by:

  1. Fetching the page's HTML and looking for <link rel="human-json">.
  2. Resolving the href to an absolute URL.
  3. Fetching and validating the JSON.

Schema

{
  "version": "0.1.1",
  "url": "https://example.com/~alice",
  "vouches": [
    {
      "url": "https://bob.example.com",
      "vouched_at": "2026-01-15"
    },
    {
      "url": "https://example.com/~charlie",
      "vouched_at": "2025-11-02"
    }
  ]
}

Fields

Field Type Required Description
version string yes Protocol version ("0.1.1"), used to determine the applicable JSON schema
url string yes Canonical URL of this site. Defines the scope of the authorship claim (see URL Matching)
vouches array no List of sites this author vouches for
vouches[].url string yes The vouched site's canonical URL
vouches[].vouched_at string yes ISO 8601 date when the vouch was made

Serving human.json

The file should be served with:

  • Content-Type: application/json
  • CORS: Access-Control-Allow-Origin: *

The CORS header is important to allow browser-based verifiers (extensions, client-side scripts) to fetch and validate the file. Since human.json is public data, there's no security risk in allowing any origin.

Trust Model

Trust is established transitively:

  1. A reader starts from a trusted seed, typically a site they already trust personally.
  2. They fetch that site's human.json and note its vouches.
  3. For each vouched site, they fetch that site's page to discover its human.json via <link rel="human-json">, then continue.
  4. Any site reachable within N hops from a trusted seed is considered vouched.

This forms a directed graph of human-verified sites, crawlable by anyone.

URL Matching

Identity is a URL prefix

A site's identity in human.json is its canonical URL, as declared in the url field. This URL defines the scope of the authorship claim: any page whose URL starts with the canonical URL is covered.

For example, if url is https://example.com/~alice, then:

  • https://example.com/~alice ✓ covered
  • https://example.com/~alice/blog ✓ covered
  • https://example.com/~alice/blog/my-post ✓ covered
  • https://example.com/~bob ✗ not covered
  • https://example.com/ ✗ not covered

A site that owns the entire domain can simply use https://example.com as its URL, which covers all pages on that origin.

Normalization rules

When comparing URLs, the following normalization rules apply:

  • Protocol must match. https://example.com and http://example.com are different identities.
  • Default ports are removed. http://example.com:80 is equivalent to http://example.com. Similarly, https://example.com:443 is equivalent to https://example.com. Non-default ports are significant.
  • Trailing slashes are stripped from the canonical URL. https://example.com/~alice/ is equivalent to https://example.com/~alice.
  • Host comparison is case-insensitive, per standard URL normalization.

Scope of a Vouch

A vouch applies to the entire URL prefix declared in the vouched site's url field.

Vouches are directional: Alice vouching for Bob does not imply Bob vouches for Alice.

Vouches are not permanent: they carry a vouched_at date, and verifiers may choose to expire trust after a certain period. To revoke a vouch, simply remove it from the vouches array. There is no explicit revocation mechanism, the absence of a vouch being sufficient.

Cycles are perfectly fine. If Alice vouches for Bob and Bob vouches for Alice, this is a normal and expected pattern. Verifiers must track visited sites to avoid infinite loops (as shown in the code below).

Verification

A verifier crawls the graph starting from one or more trusted seeds:

import json
import urllib.request
from collections import deque

import mf2py
from yarl import URL


def normalize_url(url: URL) -> URL | None:
    """Normalize a URL: lowercase host, strip default ports and trailing slash."""
    if url.scheme not in ("http", "https") or not url.host:
        return None
    return url.with_host(url.host.lower()).origin().with_path(url.path.rstrip("/"))


def is_under(page_url: URL, scope_url: URL) -> bool:
    """Check if page_url falls under the scope of scope_url (prefix match)."""
    return (
        page_url.origin() == scope_url.origin()
        and page_url.parts[: len(scope_url.parts)] == scope_url.parts
    )


def discover_human_json(url: URL) -> URL | None:
    """Fetch a URL and look for <link rel="human-json"> in the HTML."""
    parsed = mf2py.parse(url=str(url))
    hrefs = parsed.get("rels", {}).get("human-json", [])
    return URL(hrefs[0]) if hrefs else None


def fetch_human_json(url: URL) -> dict | None:
    """Fetch and validate a human.json file from a discovered URL."""
    max_size = 1024 * 1024  # 1 MB
    req = urllib.request.Request(str(url), headers={"Accept": "application/json"})
    with urllib.request.urlopen(req, timeout=10) as resp:
        data = json.loads(resp.read(max_size))
        if "version" not in data or "url" not in data:
            return None
        return data


def is_vouched(
    target: URL,
    trusted_seeds: list[URL],
    max_hops: int = 5,
) -> bool:
    """Check whether target is reachable from trusted_seeds."""
    target_normalized = normalize_url(target)
    if not target_normalized:
        return False

    visited: set[URL] = set()
    queue = deque((seed, 0) for seed in trusted_seeds)

    while queue:
        current_url, hops = queue.popleft()
        if hops > max_hops or current_url in visited:
            continue
        visited.add(current_url)

        # Discover human.json location
        human_json_url = discover_human_json(current_url)
        if not human_json_url:
            continue

        data = fetch_human_json(human_json_url)
        if not data:
            continue

        declared_url = normalize_url(URL(data["url"]))
        if not declared_url:
            continue

        for vouch in data.get("vouches", []):
            vouch_url = vouch.get("url")
            if not vouch_url:
                continue
            vouch_normalized = normalize_url(URL(vouch_url))
            if not vouch_normalized:
                continue
            if vouch_normalized == target_normalized:
                return True
            if hops + 1 <= max_hops:
                queue.append((vouch_normalized, hops + 1))

    return False

Verifiers should:

  • Respect robots.txt
  • Identify themselves with a descriptive User-Agent
  • Cache results and respect standard HTTP cache headers to avoid hammering small sites
  • Follow HTTP redirects when fetching both HTML pages and human.json files
  • Enforce a reasonable size limit when fetching human.json (e.g., 1 MB) to avoid abuse

Example

Alice has her own domain. Her site's <head> includes:

<link rel="human-json" href="/human.json">

https://alice.example.com/human.json

{
  "version": "0.1.1",
  "url": "https://alice.example.com",
  "vouches": [
    {
      "url": "https://example.com/~bob",
      "vouched_at": "2026-01-15"
    }
  ]
}

Bob is on a shared host. His pages at https://example.com/~bob/ include:

<link rel="human-json" href="/~bob/human.json">

https://example.com/~bob/human.json

{
  "version": "0.1.1",
  "url": "https://example.com/~bob",
  "vouches": [
    {
      "url": "https://charlie.example.org",
      "vouched_at": "2025-09-10"
    }
  ]
}

A reader who trusts alice.example.com can now transitively trust https://example.com/~bob (1 hop) and charlie.example.org (2 hops). Note that the vouch for Bob covers all pages under https://example.com/~bob/ but not other paths on example.com.

JSON Schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://human-json.org/schema/0.1.1.json",
  "title": "human.json",
  "description": "A protocol for humans to assert authorship of their site content and vouch for the humanity of others.",
  "type": "object",
  "required": ["version", "url"],
  "properties": {
    "version": {
      "type": "string",
      "const": "0.1.1",
      "description": "Protocol version."
    },
    "url": {
      "type": "string",
      "format": "uri",
      "description": "Canonical URL of this site. Defines the scope of the authorship claim."
    },
    "vouches": {
      "type": "array",
      "description": "List of sites this author vouches for.",
      "items": {
        "type": "object",
        "required": ["url", "vouched_at"],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "The vouched site's canonical URL."
          },
          "vouched_at": {
            "type": "string",
            "format": "date",
            "description": "ISO 8601 date (YYYY-MM-DD) when the vouch was made."
          }
        }
      }
    }
  }
}

Contributing

This is an early draft. Feedback, implementations, and proposals are welcome. The spec lives at https://codeberg.org/robida/human.json and will evolve based on real-world usage.