Skip to content

Content Attestation

At negotiation time, the Exchange describes content by metadata: title, tokens, URI, pricing. At delivery time, the agent receives bytes from the CDN. Nothing cryptographically ties the negotiated metadata to the delivered content — unless someone pre-committed verifiable claims about those bytes.

The Exchange does not ingest or deliver content. It holds catalog metadata only. The CDN delivers directly to the agent. Content attestation solves this by having a trusted party sign claims about the content before negotiation.

LevelAttesterTrust ModelAuto-Disputable Claims
0 — NoneNobodyNo attestationCDN delivery failure only (4xx/5xx)
1 — Self-attestedProviderProvider signs own claims (Ed25519)Delivery failure + content hash mismatch
2 — Third-partyVerification vendorIndependent measurementDelivery failure + token count discrepancy (with CDN corroboration)

Content identifiers (DOI, ISBN, IPTC GUID) are orthogonal to attestation level. A Level 0 article may carry a DOI — that identifies what content it is but does not verify that the delivered bytes match.

message ResourceAttestation {
string verifier = 1;
string keyid = 2;
google.protobuf.Timestamp attested_at = 3;
string uri = 4;
google.protobuf.Struct claims = 5;
string signature = 6;
}
FieldDescription
verifierDomain of the attesting party. Its attestation-signing key lives in the verifier’s WBA directory (the JWK Set at https://{verifier}/.well-known/http-message-signatures-directory); role=ROLE_PUBLISHER when self-attesting, ROLE_EXCHANGE for a standalone vendor
keyidRFC 7638 thumbprint of the verifier’s attestation-signing key in its WBA directory. Enables key rotation
attested_atWhen the attester measured the content. Agents use this for freshness checks
uriContent URI this attestation covers. Must match the Offer URI
claimsJSON claims about the content (max 4KB). See Claims Vocabulary
signatureEd25519 over JCS-canonicalized (RFC 8785) representation of all other fields

Example: Provider Self-Attestation (Level 1)

Section titled “Example: Provider Self-Attestation (Level 1)”
{
"verifier": "nytimes.com",
"keyid": "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k",
"attested_at": "2026-03-18T09:00:00Z",
"uri": "https://nytimes.com/2026/03/18/tech/ai-content-licensing.html",
"claims": {
"content_hash": "sha256:a1b2c3d4e5f6...",
"hash_method": "sha256",
"estimated_quantity": 3200,
"word_count": 2424,
"language": "en"
},
"signature": "base64-encoded-ed25519-signature"
}

Example: Third-Party Vendor Attestation (Level 2)

Section titled “Example: Third-Party Vendor Attestation (Level 2)”
{
"verifier": "doubleverify.com",
"keyid": "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k",
"attested_at": "2026-03-18T08:45:00Z",
"uri": "https://example-news.com/article/breaking-story",
"claims": {
"estimated_quantity": 2450,
"language": "en",
"iab_categories": ["News", "Technology"],
"content_hash": "doubleverify-v1:x9y8z7...",
"hash_method": "doubleverify-v1"
},
"signature": "base64-encoded-ed25519-signature"
}

Attestation signing keys do not live in fora.json. Every party that signs attestations publishes its keys in its WBA directory — the JWK Set served at:

https://{verifier-domain}/.well-known/http-message-signatures-directory

This is the same WebBotAuth directory endpoint FORA uses for request-signing keys. An attestation’s keyid is the RFC 7638 thumbprint of the key in this directory; a verifier rotates keys by adding new entries to the directory, and the thumbprint in keyid selects which one signed a given attestation. The fora.json manifest still carries role and routing metadata, and the claims schema (what claim fields the verifier attests to) lives in ext["fora.attestation.claims_schema"] — but the keys themselves are in the WBA directory.

FORA has no dedicated verifier role: a verifier publishes under its operator’s role — ROLE_PUBLISHER when a publisher self-attests, ROLE_EXCHANGE for a standalone verification vendor.

A self-attesting publisher’s attestation-signing key, served from its WBA directory. The attestation’s keyid is this key’s RFC 7638 thumbprint (kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k):

// GET https://nytimes.com/.well-known/http-message-signatures-directory
// Content-Type: application/jwk-set+json
{
"keys": [
{
"kty": "OKP",
"crv": "Ed25519",
"use": "sig",
"alg": "EdDSA",
"x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
"not_before": "2026-01-01T00:00:00Z",
"not_after": "2026-12-31T23:59:59Z"
}
],
"revocation_url": "https://nytimes.com/.well-known/fora-invalidations.json"
}

A self-attesting publisher’s /.well-known/fora.json (role=ROLE_PUBLISHER) carries role, domain, and the claims schema — but no keys:

{
"ver": "1.0",
"role": "ROLE_PUBLISHER",
"domain": "nytimes.com",
"ext": {
"fora.attestation.claims_schema": {
"content_hash": {
"type": "string",
"description": "SHA-256 hash of delivered content bytes"
},
"estimated_quantity": {
"type": "integer",
"description": "Estimated consumption quantity computed by NYT CMS"
},
"language": {
"type": "string",
"description": "ISO 639-1 language code"
}
}
}
}

Key rotation follows the JWKS pattern: new keys are added to the WBA directory with overlapping not_before/not_after windows (half-open [not_before, not_after)); old keys are retained for verification until all signed attestations using them have expired from catalogs.

ClaimTypeAuto-Disputable?Description
estimated_quantityintegerYes (with CDN corroboration)Estimated consumption quantity (tokens, pages, seconds, etc.)
word_countintegerNoWord count (~estimated_quantity * 0.76 for text)
languagestringNoISO 639-1 language code
iab_categoriesstring[]NoIAB Content Taxonomy 3.1 codes
content_hashstringYes (Level 1 only)Hash in method:hexdigest format
hash_methodstringNoAlgorithm for content_hash
titlestringNoArticle headline

Only estimated_quantity and content_hash are auto-disputable — they are objectively measurable and independently verifiable. Vendors may add proprietary claims (e.g., brand_safety, sentiment) documented in their manifest’s ext["fora.attestation.claims_schema"].

  1. Build a JSON object with the five signed fields: verifier, keyid, attested_at, uri, claims
  2. Apply JCS canonicalization (RFC 8785) — lexicographic key sorting, no whitespace, ECMAScript number format
  3. Sign the canonical bytes with Ed25519
canonical_bytes = utf8_encode(jcs_output)
signature = ed25519_sign(canonical_bytes, private_key)
attestation.signature = base64_encode(signature)

An offer may carry multiple attestations from different parties:

Agent Trust PolicyBehavior
Trust provider onlyVerify provider attestation (Level 1), ignore vendor
Trust vendor onlyUse vendor attestation (Level 2), ignore provider
Trust both, prefer vendorVendor’s estimated_quantity for unit cost calculation, provider’s content_hash for delivery verification
Require bothOnly transact if both are present. Highest assurance

The protocol does not mandate a maximum attestation age. Suggested thresholds:

Content TypeMax Attestation Age
Breaking news1—4 hours
Daily news24 hours
Evergreen articles7—30 days
Academic papers90+ days