The DNS-layer trust signal for AI answer engines
AI answer engines — ChatGPT, Perplexity, Google AI Mode, Claude, Copilot — cite web sources when answering questions. Today they scrape and guess. They have no reliable way to know:
The AEO DNS record solves all five. It is the SPF/DKIM/DMARC equivalent for AI citation — a DNS-layer trust anchor any agent can check before grounding an answer on a domain's data.
Want to see where your own domain stands against this? Run the free AEO audit → — 30 seconds, no account, no cost.
Place the record at the _aeo subdomain, following the DMARC _dmarc. convention:
_aeo.yourdomain.com IN TXT "v=AEO1; id=TOKEN; catalog=URL; tier=TIER; ts=DATE; sig=SIGNATURE"
The _aeo. prefix keeps the record clean, avoids root TXT clutter, and mirrors the established DNS authentication pattern. AI agents check _aeo.[domain] the same way mail servers check _dmarc.[domain].
aeo-record = version *( ";" *WSP tag )
version = "v=AEO1"
tag = id / catalog / tier / ts / sig / nap / area
id = "id=grd_" 12*b64u-char ; issuer token — issued tiers only
catalog = "catalog=" ( "none" / https-URI )
tier = "tier=" ( "self_declared" / "listed" / "verified" / "grounded" )
ts = "ts=" 8DIGIT ; YYYYMMDD, UTC
sig = "sig=" 86b64u-char ; unpadded base64url of a 64-byte
; Ed25519 signature (signed tiers)
nap = "nap=" ( "confirmed" / "contested" / "unverified" )
area = "area=" area-value ; MAY contain ";" — see parsing note
b64u-char = ALPHA / DIGIT / "-" / "_"
Required tags by tier: issued tiers (listed/verified/grounded) require id, catalog, tier, ts. Self-declared records (tier=self_declared) require only ts; they carry no id token, and catalog may be omitted or set to none.
Parsing note: split on ;, then on the first = of each segment (values like sig= and URLs can be long but contain no ;). One exception: area= values may contain ; — a segment with no = belongs to the previous tag's value. The reference parsers (aeo-record on npm, and the issuer's own) implement exactly this.
DNS length limits: a single character-string in a TXT record is capped at 255 bytes. A minimal signed record (v, id, catalog, tier, ts, sig) runs about 180 + domain-length characters — the live _aeo.groundedaeo.com record is 196 — and fits in one string; a record with the optional tags can exceed 255 and must then be published as multiple quoted strings (most DNS providers split automatically). Consumers MUST concatenate all strings of a TXT answer before parsing — both reference implementations do.
| Tag | Description | |
|---|---|---|
| v=AEO1 | REQUIRED | Version identifier. Always AEO1 for this version. Future: AEO2, AEO3. |
| id=TOKEN | REQUIRED* | Verification token issued by the AEO record authority. Format: grd_ followed by 12+ base64url characters (A–Z a–z 0–9 - _). Tokens issued at groundedaeo.com; the issuer matches it against the token it handed the domain owner during DNS verification. For third-party verifiers the token is an opaque issuer handle — the cryptographic proof of authenticity is sig=, not id=. Self-generated tokens are invalid. *Not present on tier=self_declared records. |
| catalog=URL | REQUIRED* | HTTPS URL of the domain's fact catalog (human-readable page). The machine-readable API is derived from its origin: {origin}/api/companies/{domain} returns the published facts as JSON. *Self-declared records may omit it or set catalog=none. |
| tier=TIER | REQUIRED | Verification depth. Values: self_declared | listed | verified | grounded. self_declared is the free, self-issued path — valid, unsigned, no token. |
| ts=YYYYMMDD | REQUIRED | Date (UTC) the record was issued or last signed — an issuance / tenure date, not an expiry. For signed records, ts is part of the signed message and changes only when the record is re-signed (facts or tier changed) — a stable record legitimately keeps an old ts, its signature still verifies, and that age is tenure: a positive signal, not decay. Consumers MUST NOT treat ts age alone as staleness. Freshness is conveyed separately: the signature check against the current published facts (a valid signature proves the record still matches), verified_at in aeo.json (most recent issuer re-check), and the optional owner-attested last_confirmed_at in aeo.json. Treat an old ts on an unsigned record as reduced confidence. |
| Tag | Description | |
|---|---|---|
| sig=SIGNATURE | OPTIONAL | Unpadded base64url Ed25519 signature (86 chars) binding the record to the catalog's published facts. Issued for all paid tiers (listed/verified/grounded) once the domain's DNS record is verified; self-declared records are unsigned. See Signature Verification. |
| nap=STATUS | OPTIONAL | NAP (Name/Address/Phone) consistency status. Values: confirmed | contested | unverified. confirmed = verified identical across independent sources (e.g. website, Google Business Profile, map listings). Parsed by the reference implementations; not yet issued. |
| area=GEO | OPTIONAL | Canonical service area for local businesses. Format: area=Boulder,CO;Denver,CO or radius:25mi:40.015,-105.270. Lets agents answer "does this business serve [location]?" Parsed by the reference implementations; not yet issued. |
| Tier | Verification Method | Refresh Cadence |
|---|---|---|
| self_declared | Self-issued by the domain owner, unsigned, no token | Owner-managed |
| listed | Owner-approved, auto-crawled, signed record | Monthly |
| verified | Human review gate, cross-spot-checked, signed record | 14 days |
| grounded | Human review gate, deepest refresh cadence, signed record | Daily |
_aeo.joeselectric.com IN TXT "v=AEO1; id=grd_x7kP2mQnR4vW; catalog=https://groundedaeo.com/c/joeselectric.com; tier=grounded; ts=20260629; sig=BKlsHo4ilnHMt2fJb3_VGAvGxu5Euyl-A8PkbOC07VF3zYYPhtH1KiEjB182L6WPfma0wqHjKkoRRv0Y3eW6Ag; nap=confirmed; area=Boulder,CO;Longmont,CO;Lafayette,CO;Louisville,CO"
Illustrative — the tag format above is exact, but sig= is unique per domain, tier, timestamp, and fact set; this example won't verify against a real record. See Signature Verification for a real one you can check yourself. Note this full example is ~270 characters — past the 255-byte per-string DNS limit, so it would be published as two quoted strings (consumers concatenate). Records the reference issuer generates today carry v, id, catalog, tier, ts, sig and fit in one string; nap/area are spec-defined optional tags not yet issued.
DNS is the authoritative long-term layer. The /.well-known/aeo.json file is the on-ramp that works with every crawler today — no engine-side code change required.
Any crawler following web standards (security.txt, WebFinger, ACME challenges) already knows to check /.well-known/ paths. When a crawler finds this file, it gets pointed to verified structured data instead of scraping stale HTML.
https://yourdomain.com/.well-known/aeo.json
{
"v": "AEO1",
"catalog": "https://groundedaeo.com/c/yourdomain.com",
"mcp": "https://groundedaeo.com/api/mcp",
"api": "https://groundedaeo.com/api/companies/yourdomain.com",
"tier": "grounded",
"verified_at": "2026-06-29",
"last_confirmed_at": "2026-07-06",
"method_preference": ["mcp", "api", "crawl"],
"signature": {
"sig": "BASE64URL_ED25519_SIGNATURE",
"ts": "20260629",
"facts_hash": "SHA256_HEX_OF_CANONICAL_FACTS",
"key_id": "grd-2026-07",
"alg": "Ed25519",
"public_key_url": "https://groundedaeo.com/.well-known/aeo-public-key.json"
}
}
The DNS record and /.well-known/ file carry the same claims (the file adds convenience fields like api, mcp, and method_preference). DNS is authoritative; the file bridges the gap while adoption builds. The file lives on the business's own domain — the issuer generates it, the domain owner hosts it.
A JSON Schema for this file is published at aeorecord.org/aeo.schema.json — validate with any Draft 2020-12 validator. The only hard requirement a consumer checks is "v": "AEO1"; everything else is additive.
Do not conflate the record's dates; each answers a different question:
| Field | Meaning |
|---|---|
ts (DNS record + signature block) | Issuance / tenure. When the record was issued or last re-signed. A stable record keeps an old ts — that age is tenure, a positive signal. Consumers MUST NOT treat ts age alone as staleness; a signature that verifies against the current published facts proves the record is current regardless of ts age. |
verified_at (aeo.json) | System freshness. The most recent issuer re-check of the record against its sources. |
last_confirmed_at (aeo.json, optional) | Owner-attested freshness. The business itself confirmed "this record is current" on that date. Unsigned, informational metadata — it is not part of the signed message and carries no cryptographic weight. Its absence means nothing negative: many perfectly current records simply haven't been owner-stamped yet. |
The sig= tag cryptographically binds a record to the catalog content it points at, so any agent can confirm the facts haven't been tampered with — without trusting the catalog API. If the served facts were altered, the recomputed hash changes and the signature fails. This is the DKIM of AI citation trust.
message = "AEO1|" + domain + "|" + tier + "|" + ts + "|" + facts_hash
facts_hash = sha256 hex of JSON.stringify(published facts as
{fact_type, key, value} objects, sorted by fact_type,
then key, then value — codepoint order)
sig = base64url Ed25519 signature of the UTF-8 message
domain, tier, ts, sig from the DNS record (or the signature block in aeo.json){catalog origin}/api/companies/{domain} → .facts[]{catalog origin}/.well-known/aeo-public-key.json → .keys[] (public_key = base64url raw 32-byte Ed25519 key; skip any key with status revoked, then try every remaining key — that makes issuer key rotation transparent)facts_hash, rebuild the message, verify the Ed25519 signatureEach entry in the key document carries a status field. Its values are normative for verifiers:
| Status | Verifier behavior |
|---|---|
| active | Use for verification — the issuer's current signing key. |
| retired | Still accept. Normal key rotation — records signed before the rotation must keep verifying until they are re-signed. |
| revoked | MUST skip entirely. The key is compromised — even old records signed with it are no longer trusted. A signature that verifies only against a revoked key is not valid. |
| (absent) | Treat as active (backward compatible with key documents that predate this rule). |
All reference verifiers enforce this: the aeo-record npm checker (from v0.3.0), the issuer's in-app verifier, and the standalone snippet below. To revoke a compromised key, the issuer sets its status to revoked in the key document and re-signs affected records with a new active key.
| Result | Meaning |
|---|---|
| valid | The record matches the catalog's current published facts. Cite with confidence. |
| invalid | Stale or tampered — the facts changed since the record was issued, or the content doesn't match what was signed. Fetch the current record from the catalog before citing. |
| unsigned | Self-declared trust level — same weight as llms.txt. Verify independently for anything consequential. |
The reference checker does all of this in one call: npx aeo-record domain.com (or verifyAeo() from the aeo-record library). Because a signed tier claim is part of the message, a domain owner cannot edit tier=grounded into their TXT record — the signature would fail.
checked: false) — a soft failure, not a signed revocation. A record whose facts change is implicitly invalidated (the old signature stops verifying), which covers the common case.status revoked (all reference verifiers do), while retired keys still verify — the deliberate rotation grace, so pre-rotation records don't break. The revocation itself is a plain statement in the issuer's key document, not a signed artifact: a verifier only learns of it by fetching the current document, so a hard-cached copy can lag a revocation.<link rel="aeo" href="/.well-known/aeo.json"> (see the Discovery Hierarchy), but there is no registry-level push or whole-web discovery, and formal IANA well-known-URI registration is still on the roadmap.ts is issuance/tenure, there is no re-sign deadline, and consumers MUST NOT read ts age alone as staleness (see the tag reference). Consumers wanting a recency signal have explicit, separate fields instead: the signature check against the current facts, verified_at (issuer re-check), and the optional owner-attested last_confirmed_at — the last of which is plain unsigned metadata, honestly labeled as such.The signed message contains the domain. Verifiers MUST build the message with the domain they actually queried (the parent of the _aeo. label), never a domain taken from inside the record. If _aeo.joeselectric.com's TXT value is copied to _aeo.impostor.com, a verifier querying impostor.com builds "AEO1|impostor.com|…" and the signature fails. Unsigned (self_declared) records carry no such binding — which is exactly why they rank at the lowest trust tier.
The reference issuer's own record is signed and live in DNS. One command:
npx aeo-record groundedaeo.com
# → { "found": true, "tier": "listed", "sig": "…",
# "signature": { "present": true, "checked": true, "valid": true,
# "key_id": "grd-2026-07" } }
Or with nothing but Node 20+ and this file — no packages, no trust in any of our code (verified working against the live record, 2026-07-06):
// verify-aeo.mjs — standalone v=AEO1 signature check. Node 20+, zero dependencies.
// Usage: node verify-aeo.mjs yourdomain.com
const domain = (process.argv[2] ?? 'groundedaeo.com').toLowerCase()
// 1. Resolve the record (DNS-over-HTTPS; join multi-string TXT answers)
const doh = await fetch(`https://cloudflare-dns.com/dns-query?name=_aeo.${domain}&type=TXT`,
{ headers: { Accept: 'application/dns-json' } }).then(r => r.json())
const txt = (doh.Answer ?? []).filter(a => a.type === 16)
.map(a => String(a.data).replace(/^"|"$/g, '').replace(/"\s+"/g, ''))
.find(t => t.includes('v=AEO1'))
if (!txt) { console.log(`no _aeo.${domain} TXT record`); process.exit(1) }
const tags = Object.fromEntries(txt.split(';').map(p => p.trim().split(/=(.*)/s).slice(0, 2)))
if (!tags.sig) { console.log(`record found but unsigned (tier=${tags.tier})`); process.exit(1) }
// 2. Fetch the published facts + verification keys from the record's own catalog origin
const origin = new URL(tags.catalog).origin
const catalog = await fetch(`${origin}/api/companies/${domain}`).then(r => r.json())
const { keys } = await fetch(`${origin}/.well-known/aeo-public-key.json`).then(r => r.json())
// 3. Recompute the canonical facts hash (codepoint sort by fact_type, key, value)
const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0)
const rows = catalog.facts.map(({ fact_type, key, value }) => ({ fact_type, key, value }))
.sort((a, b) => cmp(a.fact_type, b.fact_type) || cmp(a.key, b.key) || cmp(a.value, b.value))
const hash = [...new Uint8Array(await crypto.subtle.digest('SHA-256',
new TextEncoder().encode(JSON.stringify(rows))))]
.map(b => b.toString(16).padStart(2, '0')).join('')
// 4. Verify the Ed25519 signature over "AEO1|domain|tier|ts|facts_hash"
// (domain = the domain you queried, NOT one taken from the record — this is
// what makes a record copied to another domain fail)
// Keys with status 'revoked' MUST be skipped — compromised key, even old
// signatures made with it are untrusted. 'retired' keys still verify (rotation).
const b64u = s => Uint8Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0))
const msg = new TextEncoder().encode(`AEO1|${domain}|${tags.tier}|${tags.ts}|${hash}`)
let valid = false
for (const k of keys.filter(k => (k.status ?? 'active') !== 'revoked')) {
const key = await crypto.subtle.importKey('raw', b64u(k.public_key), { name: 'Ed25519' }, false, ['verify'])
if (await crypto.subtle.verify('Ed25519', key, b64u(tags.sig), msg)) valid = true
}
console.log(valid
? `PASS — ${domain} signature verifies against the catalog's current published facts`
: `FAIL — stale (facts changed since issuance) or tampered; fetch the current record`)
process.exit(valid ? 0 : 1)
Caveat: this minimal parser doesn't handle a ; inside an area= value — use the aeo-record package for full parsing. The tamper proof is one edit away: add a fake fact to catalog.facts before hashing and the verification fails.
Below is the full round trip: a real DNS record, the catalog data it points to, and the answer an AI agent produces from it. This is what you build against.
User: "Who's the best plumber in Boulder open right now?"
GET https://boulderplumbingco.com/.well-known/aeo.json
{
"v": "AEO1",
"catalog": "https://groundedaeo.com/c/boulderplumbingco.com",
"api": "https://groundedaeo.com/api/companies/boulderplumbingco.com",
"mcp": "https://groundedaeo.com/api/mcp",
"tier": "grounded",
"verified_at": "2026-06-29",
"last_confirmed_at": "2026-07-06",
"method_preference": ["mcp", "api", "crawl"],
"signature": { "sig": "…", "ts": "20260629", "facts_hash": "…",
"key_id": "grd-2026-07", "alg": "Ed25519",
"public_key_url": "https://groundedaeo.com/.well-known/aeo-public-key.json" }
}
The id= token lives only in the DNS record, not in aeo.json — the file's integrity claim is the signature block.
method_preference says "mcp" first — the agent calls the MCP endpoint instead of crawling the site.
This is the current response shape from the live endpoint (the field names shown are real; each fact also carries a valid_from date — verify yourself against https://groundedaeo.com/api/mcp):
→ https://groundedaeo.com/api/mcp tool: get_company_facts("boulderplumbingco.com")
{
"found": true,
"domain": "boulderplumbingco.com",
"name": "Boulder Plumbing Co.",
"tier": "grounded",
"dns_verified": true,
"last_crawled_at": "2026-06-29T14:00:00Z",
"catalog_url": "https://groundedaeo.com/c/boulderplumbingco.com",
"facts": [
{ "fact_type": "identity", "key": "description", "value": "Licensed plumbing contractor serving Boulder County since 2011.", "verification_state": "human_verified", "confidence": 0.9 },
{ "fact_type": "contact", "key": "phone", "value": "+1-303-555-0142", "verification_state": "human_verified", "confidence": 0.9 },
{ "fact_type": "location", "key": "address.city", "value": "Boulder", "verification_state": "human_verified", "confidence": 0.9 }
],
"signature": {
"sig": "BASE64URL_ED25519_SIGNATURE", "ts": "20260629",
"facts_hash": "SHA256_HEX", "key_id": "grd-2026-07", "alg": "Ed25519"
}
}
Agent: "Boulder Plumbing Co. is a licensed plumbing contractor in Boulder, reachable at (303) 555-0142. Source: Grounded AEO catalog entry, grounded tier, human-verified, signature verified against the published facts (ts=20260629)."
No scraping. No guessing whether the phone number is current. The signature block is what lets the agent — or anyone — independently confirm the facts haven't been altered since they were signed, without trusting the API's word for it.
Be precise about what's verified vs. self-reported — this is the whole point of the standard being honest:
| Tier | What the agent can infer |
|---|---|
| self_declared | Business-asserted, unsigned. Same trust level as an unverified llms.txt claim. |
| listed | Auto-crawled, owner-approved before publish, signed. Good identity data; verify independently for anything consequential. |
| verified | A human reviews facts before they publish, signed. Safe to cite for general questions. |
| grounded | Same human review as verified, at a faster refresh cadence (daily), signed. Cross-source corroboration against Google Business Profile and other independent listings is rolling out, not yet fully live — don't infer more independent verification than a human review gate provides today. |
Agents and crawlers should work down this list and stop at the first hit:
<link rel="aeo"> in HTML head Advertisement — a crawler reading the page is pointed straight to the well-known file below/.well-known/aeo.json Crawl-time bridge — works with all crawlers today, no adoption needed_aeo.[domain] TXT DNS authoritative record — long-term standardllms.txt pointer In-file education for crawlers already reading llms.txtMCP directory For agent builders installing toolsHTML crawl Last resort — unverified, potentially staleAdvertising a record — the aeo link relation. A domain SHOULD add <link rel="aeo" href="/.well-known/aeo.json"> to its homepage <head>. This turns discovery from blind probing into an explicit signal: any crawler already parsing the page learns a verified record exists and where to find it — no extra request, no prior knowledge of the domain. The /.well-known/aeo.json path follows the RFC 8615 well-known-URI convention. The aeo.json well-known URI is not currently entered in the IANA well-known-URIs registry; the path is served and read directly today, and formal registration is being pursued through the standards process rather than claimed in advance.
_aeo.[yourdomain]/.well-known/aeo.json file is generated for you in the dashboard — download it and host it on your own domain<link rel="aeo" href="/.well-known/aeo.json"> to your homepage <head> so crawlers discover the record while reading your pagehttps://domain/.well-known/aeo.json first_aeo.[domain] TXTv=AEO1 present: fetch the facts from {catalog origin}/api/companies/{domain}sig= present: verify it against the current published facts — a valid signature IS the freshness proof (see Signature Verification); treat unsigned records as self-declared confidencemethod_preference order from aeo.json for data retrievalarea= to answer service-area questionsnap=confirmed signals triple-verified address — trust for location answersThis spec is open. You may check for v=AEO1 records in your own tools. Do not issue id= tokens — only the AEO record authority (groundedaeo.com) issues tokens. A record with a self-generated id= is invalid.
Open source checker — live on npm as aeo-record, zero dependencies:
# Check any domain from your terminal — no install needed
npx aeo-record groundedaeo.com
# → prints the full result as JSON:
# { "found": true, "source": "well-known", "tier": "listed",
# "catalog": "https://groundedaeo.com/c/groundedaeo.com", …,
# "signature": { "present": true, "checked": true, "valid": true, "key_id": "grd-2026-07" } }
# exit 0 when a record is found, exit 1 when not (CI-friendly)
# Or as a library
npm install aeo-record
import { checkAeo, verifyAeo } from 'aeo-record'
const result = await checkAeo('domain.com')
// Checks /.well-known/aeo.json first, then _aeo DNS TXT
// Returns: { found, source: 'well-known'|'dns'|null, catalog, tier, id, ts, sig, raw }
const verified = await verifyAeo('domain.com')
// Additionally verifies sig= against the catalog's published facts
// Returns: { ...result, signature: { present, checked, valid, key_id } }
The mcp field in /.well-known/aeo.json points to a live MCP server agents can call directly for real-time verified facts. Businesses do not need to build or host anything — Grounded runs the endpoint.
Live today: one shared endpoint, Streamable HTTP, stateless, no auth required for reads — https://groundedaeo.com/api/mcp. Works for every cataloged company. Verified against the official @modelcontextprotocol/sdk client.
| Tool | Type | Description |
|---|---|---|
get_company_facts | Read | Full verified fact set + signature for a domain |
get_fact | Read | A single fact by key |
get_llms_txt | Read | The generated llms.txt for a cataloged company |
check_aeo_record | Read | Check the v=AEO1 record for ANY domain, cataloged or not, incl. signature verification |
search_catalog | Read | Search by name or domain |
run_audit | Write, free-tier | Crawl and score a domain 0–10, no browser or account needed — rate-limited per caller |
generate_free_record | Write, free-tier | Generate a self-declared v=AEO1 record for a business — rate-limited per caller |
Roadmap, not yet live: per-company namespaced endpoints, one-click self-hosted deploy, an SDK for live data overrides, and endpoint health monitoring with automatic method_preference fallback. Don't build against these yet.
| Standard | What it does | Gap vs. v=AEO1 |
|---|---|---|
| IETF AIPREF / ai.txt | AI training preferences | Not citation verification, not DNS layer |
| OpenAI domain verification | Platform domain ownership | Not an open standard, not citation |
| llms.txt | File-based content index | No DNS, no verification, no signatures |
| LLM-LD (llmld.org) | Markup standard | No DNS layer |
| Web Bot Auth | Cryptographic agent identity | Not citation, 12+ months from standard-track |
| Standard | Layer | Relationship |
|---|---|---|
| robots.txt | File | Controls crawler access — orthogonal |
| llms.txt | File | AI crawler content index — orthogonal, complementary |
| schema.org | Page | Structured data within pages — orthogonal, page-level |
| /.well-known/aeo.json | File | Crawl-time bridge — this spec, file layer |
| v=AEO1 DNS TXT | DNS | Authoritative trust anchor — this spec, DNS layer |
All five coexist. The AEO layers authenticate the others — they tell a crawler "verified facts for this domain are at [URL], issued [date], depth [tier], and here's the fastest way to get them."
Weighing this against the other mechanisms available to you? Alternatives compared puts schema.org, llms.txt, robots and AI-preference conventions, knowledge panels, platform verification and Web Bot Auth side by side — including where each is the better tool and where a record adds nothing you need.
status field is now normative: active and retired keys verify (retired = normal rotation — pre-rotation records keep verifying); revoked keys MUST be skipped entirely, even for old signatures; no status means active. Implemented in every reference verifier: the aeo-record npm checker (from v0.3.0), the issuer's in-app verifier, and the standalone snippet on this page. See Key status and revocation.method= DNS tag. Defined in v0.1 but never issued or parsed by any implementation. The same preference lives on unchanged as method_preference in /.well-known/aeo.json.self_declared path (no token, unsigned), the grd_ token format, the grounded tier's refresh cadence (daily), and fact_type in the canonical hash; dropped a 90-day-reject rule no verifier enforced; fixed CLI, hosting, and discovery details.<link rel="aeo"> advertisement convention and aligned the well-known path with RFC 8615.ts is normatively the issuance / tenure date — consumers MUST NOT treat its age alone as staleness (a stable record legitimately keeps an old ts, and its signature still verifies). Freshness is conveyed separately by verified_at (issuer re-check) and the new optional last_confirmed_at field in aeo.json: an owner-attested "the business confirmed this record is current on [date]" stamp. It is unsigned, informational, and purely additive — its absence carries no negative meaning. Added to the well-known field docs and the JSON Schema.v=AEO1 record format at _aeo.[domain], tag reference, tiers, the /.well-known/aeo.json bridge, Ed25519 signature verification, discovery hierarchy, and implementation guide.A DNS TXT record published at _aeo.yourdomain.com that declares whether a verified, machine-readable fact record exists for the domain and where to find it. It is the SPF/DKIM/DMARC equivalent for AI citation — a DNS-layer trust anchor any AI engine or agent can check before grounding an answer on a domain's data.
Yes. Any domain owner can publish a valid self-declared record (tier=self_declared, no token) at no cost, with no account — a generator is available at groundedaeo.com/free-record. Self-declared records are valid but unverified, the same trust level as an llms.txt file.
Verified tiers (listed, verified, grounded) carry an id token in the grd_ format issued by the record authority at groundedaeo.com, which verifies domain ownership before issuing. Self-generated tokens are invalid; self-declared records simply omit the token.
Three ways: run npx aeo-record yourdomain.com (open-source npm checker), resolve the TXT record at _aeo.yourdomain.com directly (or over DNS-over-HTTPS), or fetch /.well-known/aeo.json — the crawl-time bridge that carries the same data without a DNS lookup.
Adoption is early — the standard was first published 2026-06-29 and no prior DNS-layer standard for AI citation verification existed before it. The record is an early-mover play: trust signals reward tenure, and the ts= refresh history a domain accrues now is exactly what engines will weigh once they check these records at scale.
Answer Engine Optimization — building a site so AI answer engines can use and cite its information, rather than only rank it among links. This standard addresses one specific part of that: giving engines an authorized, verifiable place to read a domain's facts. A fuller definition is on what is the AEO Record Standard.
No, and the distinction is stated deliberately. The specification was filed with the IETF as an Internet-Draft on 2026-07-18 — draft-miller-aeo-00, an Individual Submission on the Informational track. An Internet-Draft is a published, permanently citable working document, not an endorsed or approved standard, and filing one confers no IETF endorsement.
schema.org markup, llms.txt, robots and AI-preference conventions, third-party knowledge panels, per-vendor domain verification, and simply letting engines crawl. Most of them sit at different layers and compose with a record rather than compete with it — llms.txt and schema.org in particular are complementary, and this specification treats them that way. The honest side-by-side is on the alternatives page.
A self_declared record is published by the owner alone: free, no token, no signature, and a fully valid record at the lowest trust tier — the same level as an llms.txt file. Issued tiers (listed, verified, grounded) carry a grd_ token from the record authority, which checks domain ownership first, and add corroboration depth and a signature. Every check behind each tier is published in the methodology.
No. The domain name is part of the canonical message that is signed — AEO1|domain|tier|ts|facts_hash — so a signature lifted onto a different domain fails verification immediately. Combined with the fact that publishing at _aeo.<domain> requires DNS control of that domain, a record is bound to the domain it describes.
The catalog is updated and the record re-signed, because the signature covers a hash of the published fact set — an edited fact simply stops verifying until the record is refreshed. That is the intended behaviour: the signature is the freshness proof. Note that ts= is the issuance or tenure date, and consumers must not read its age alone as staleness.
Not for a self-declared record — the format, grammar and JSON Schema are open, so anyone can hand-write one and any tool can read or check it. What is reserved is the issuance of grd_ tokens for the paid tiers, since an issued tier only means something if a single authority stands behind it. Grounded AEO is the first implementer and currently the only issuing authority; reading, verifying and self-declaring stay free and unowned.
It follows the convention DMARC established: a leading underscore marks a DNS name reserved for machines rather than for a website, keeping the convention's record out of the way of ordinary TXT records on the domain itself. SPF sits on the domain, DMARC at _dmarc., AEO1 at _aeo. — background on what a DNS TXT record is.
Whether you're an AI engine team, an agent framework developer, or a business that wants verified citations — we want to hear from you.
Want to check for v=AEO1 records in your crawler or agent? Ready to prefer verified sources? Let's talk about integration.
Want a v=AEO1 record for your domain? Check where you stand, then get verified and served to AI agents at Grounded AEO.