From e0a87d2bee4079fd41157ab0a72abc302cf9f1d0 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Thu, 14 May 2026 09:34:02 -0400 Subject: [PATCH 1/5] docs(brand-protocol): RFC for brand verification surface (#4521) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Draft RFC for a federated trust capability on the brand-agent — three interrogative tools that let partners ask the brand authoritatively whether something belongs to it. Reframes the email-based self-healing SHOULD from PR #4505 as a richer pull-based DRM-for-brand-identity surface. New tasks (all brand-protocol, advertised in get_adcp_capabilities): - verify_subsidiary_claim — replaces crawl inference for is-this-a-leaf - verify_property — authoritative ownership of websites/apps/etc - verify_trademark — licensing + jurisdiction + Nice class Shared VerificationStatus enum captures rich state crawl cannot express: owned / pending_review / disputed / not_ours / licensed_in / licensed_out / unknown. Public/authorized tier split mirrors get_brand_identity. Cross-protocol: brand.json Conformance gains a SHOULD that when a house publishes a brand-agent advertising these tasks, consumers prefer the agent's signed response over crawl-based mutual-assertion inference. The email-notification SHOULD continues to apply as a fallback when no brand-agent is advertised. The RFC supersedes the typed-notification- endpoint design originally sketched in #4521 — the verification surface is interrogative, not notification-based. Additive — no changes to brand.json itself; no migration burden. Co-Authored-By: Claude Opus 4.7 (1M context) --- .changeset/brand-verification-rfc.md | 18 ++ docs.json | 6 + docs/brand-protocol/brand-json.mdx | 1 + .../proposals/brand-verification-rfc.mdx | 215 ++++++++++++++++++ docs/brand-protocol/tasks/verify_property.mdx | 116 ++++++++++ .../tasks/verify_subsidiary_claim.mdx | 121 ++++++++++ .../brand-protocol/tasks/verify_trademark.mdx | 145 ++++++++++++ .../source/brand/verification-status.json | 25 ++ .../source/brand/verify-property-request.json | 47 ++++ .../brand/verify-property-response.json | 87 +++++++ .../verify-subsidiary-claim-request.json | 31 +++ .../verify-subsidiary-claim-response.json | 77 +++++++ .../brand/verify-trademark-request.json | 42 ++++ .../brand/verify-trademark-response.json | 102 +++++++++ 14 files changed, 1033 insertions(+) create mode 100644 .changeset/brand-verification-rfc.md create mode 100644 docs/brand-protocol/proposals/brand-verification-rfc.mdx create mode 100644 docs/brand-protocol/tasks/verify_property.mdx create mode 100644 docs/brand-protocol/tasks/verify_subsidiary_claim.mdx create mode 100644 docs/brand-protocol/tasks/verify_trademark.mdx create mode 100644 static/schemas/source/brand/verification-status.json create mode 100644 static/schemas/source/brand/verify-property-request.json create mode 100644 static/schemas/source/brand/verify-property-response.json create mode 100644 static/schemas/source/brand/verify-subsidiary-claim-request.json create mode 100644 static/schemas/source/brand/verify-subsidiary-claim-response.json create mode 100644 static/schemas/source/brand/verify-trademark-request.json create mode 100644 static/schemas/source/brand/verify-trademark-response.json diff --git a/.changeset/brand-verification-rfc.md b/.changeset/brand-verification-rfc.md new file mode 100644 index 0000000000..d985133fb0 --- /dev/null +++ b/.changeset/brand-verification-rfc.md @@ -0,0 +1,18 @@ +--- +--- + +Draft RFC for the brand verification surface — three interrogative brand-agent tasks that let partners ask the brand authoritatively whether something belongs to it. + +The RFC lives at `docs/brand-protocol/proposals/brand-verification-rfc.mdx`. Tracks the federated trust capability discussed in [#4521](https://github.com/adcontextprotocol/adcp/issues/4521) and supersedes the email-based self-healing path landed in PR #4505. Not yet normative — needs spec-owner sign-off before any agent implementations standardize. + +New tasks proposed (all on the brand protocol surface, advertised in `get_adcp_capabilities` `supported_tasks`): + +- `verify_subsidiary_claim` — "Is this brand a subsidiary of yours?" Replaces crawl-based mutual-assertion inference with the brand-agent's authoritative answer, including the `pending_review` and `disputed` states crawl cannot express. +- `verify_property` — "Is this site / app / property actually one of yours?" Returns ownership + the property-relationship enum (`owned` / `direct` / `delegated` / `ad_network`) plus optional per-use-case authorization. +- `verify_trademark` — "Is this trademark one of yours?" Returns ownership, licensing relationship, jurisdictions, Nice classes, and optional use-case authorization. + +Shared `VerificationStatus` enum captures the rich state surface (`owned`, `pending_review`, `disputed`, `not_ours`, `licensed_in`, `licensed_out`, `unknown`). Public/authorized tier split mirrors `get_brand_identity`. + +Cross-protocol Conformance addition to `brand.json`: when a house publishes a brand-agent advertising these tasks, consumers SHOULD prefer the agent's signed response over crawl-based mutual-assertion inference. The crawl path remains the fallback when the agent is unreachable or returns `unknown`. + +Schema additions: `core/verification-status.json`, three request schemas, three response schemas. No changes to `brand.json` itself. Additive — every existing publisher and every existing brand-agent continues to work unchanged. diff --git a/docs.json b/docs.json index becb03393f..a9da76db80 100644 --- a/docs.json +++ b/docs.json @@ -498,6 +498,9 @@ "group": "Tasks", "pages": [ "docs/brand-protocol/tasks/get_brand_identity", + "docs/brand-protocol/tasks/verify_subsidiary_claim", + "docs/brand-protocol/tasks/verify_property", + "docs/brand-protocol/tasks/verify_trademark", "docs/brand-protocol/tasks/get_rights", "docs/brand-protocol/tasks/acquire_rights", "docs/brand-protocol/tasks/update_rights" @@ -1064,6 +1067,9 @@ "group": "Tasks", "pages": [ "docs/brand-protocol/tasks/get_brand_identity", + "docs/brand-protocol/tasks/verify_subsidiary_claim", + "docs/brand-protocol/tasks/verify_property", + "docs/brand-protocol/tasks/verify_trademark", "docs/brand-protocol/tasks/get_rights", "docs/brand-protocol/tasks/acquire_rights", "docs/brand-protocol/tasks/update_rights" diff --git a/docs/brand-protocol/brand-json.mdx b/docs/brand-protocol/brand-json.mdx index 51371097e3..51b83a5985 100644 --- a/docs/brand-protocol/brand-json.mdx +++ b/docs/brand-protocol/brand-json.mdx @@ -1183,6 +1183,7 @@ These invariants MUST be enforced by validators and crawlers; JSON Schema cannot **Self-healing** - **Leaf-only edges SHOULD trigger notification.** When a consumer encounters a leaf claiming `house_domain: X` with no reciprocal entry on X, the consumer SHOULD notify X's `contact.email` so the house team can complete the reciprocation. Notifying parties MUST rate-limit per `{leaf, house}` pair to avoid flooding. +- **Agent-preferred verification (proposed — see [brand verification RFC](/docs/brand-protocol/proposals/brand-verification-rfc)).** When the named house publishes a brand-agent advertising `verify_subsidiary_claim` (or `verify_property` / `verify_trademark`) in its `supported_tasks`, consumers SHOULD prefer the agent's authoritative response over crawl-based mutual-assertion inference. The agent's signed response wins; the crawl path remains as fallback when the agent is unreachable or returns `unknown`. Not yet normative; the email notification SHOULD continues to apply. ## Prior art diff --git a/docs/brand-protocol/proposals/brand-verification-rfc.mdx b/docs/brand-protocol/proposals/brand-verification-rfc.mdx new file mode 100644 index 0000000000..f38da630a0 --- /dev/null +++ b/docs/brand-protocol/proposals/brand-verification-rfc.mdx @@ -0,0 +1,215 @@ +--- +title: "RFC — Brand verification surface" +description: "Proposal for a federated trust surface on the brand-agent. Three interrogative tools — verify_subsidiary_claim, verify_property, verify_trademark — let partners ask the brand authoritatively whether something belongs to it." +"og:title": "AdCP — Brand verification RFC" +--- + + +**RFC — discussion in progress.** Not yet normative. Tracks the federated trust capability discussed in [#4521](https://github.com/adcontextprotocol/adcp/issues/4521) and the spec-side follow-ups from PR [#4505](https://github.com/adcontextprotocol/adcp/pull/4505). + + +## Summary + +Add three interrogative tools to the brand-protocol surface that let partners ask a brand's authoritative agent whether something belongs to it: + +- **`verify_subsidiary_claim`** — "Is this brand a subsidiary of yours?" +- **`verify_property`** — "Is this site / app / property actually one of yours?" +- **`verify_trademark`** — "Is this trademark one of yours?" + +These collapse the original [self-healing notification](/docs/brand-protocol/brand-json#self-healing-through-notification) surface (PR #4505 landed an email-based SHOULD) into a richer **pull-based federated trust** capability. A consumer (DSP, crawler, partner agent) detecting a leaf-only edge no longer pushes a notification at the house and waits — it asks the brand's agent directly and gets an authoritative answer. + +The mutual-assertion crawl model stays the decentralized backstop. The brand-agent verification path is the federated alternative for brands that opt in. + +## Motivation + +`brand.json`'s mutual-assertion model is decentralized — two parties publish their halves at well-known URLs and a consumer crawls both to confirm. It's the right primitive for the open web, but it has known gaps: + +- **Two-state visibility.** Crawling answers "mutual or not." It can't surface *pending review*, *disputed*, *licensed in*, or *under M&A*. Brands need a richer state surface. +- **TTL-bound freshness.** A consumer's view is only as fresh as its last crawl. M&A-driven changes don't propagate until the next refresh. +- **One-sided pessimism.** When a leaf claims a parent and the parent hasn't reciprocated, the leaf gets downgraded to "claimed, unverified." Even if the parent's intent is to confirm, the protocol can't surface that intent before the file edit propagates. +- **No partner-side double-check primitive.** A "good partner" — say, a DSP about to extend governance trust through a leaf — has no way to ask the brand "is this really yours, with my use case in mind?" before acting. + +The brand-agent surface (variant 3 / typed `agents[]`) already exists for this kind of authoritative-source pattern. `get_brand_identity` is the Tier 1 implementation. The verification tools are Tier 2. + +This is **DRM-shaped for brand identity**: the brand owns its own answers to "what's mine," and authorized partners can ask before they act. It's not gating access to public data — the data is still public — it's providing nuanced, authoritative answers that crawling cannot. + +## Proposal + +### Three tools, one shape + +Each tool follows the same shape: + +- **Input:** the claim being verified (subsidiary domain, property, trademark) +- **Output:** an enum status from a shared `VerificationStatus` vocabulary, plus tool-specific context + +The status enum captures the rich state the crawl model can't express: + +| Status | Meaning | +|---|---| +| `owned` | Definitively belongs to this brand, currently. | +| `pending_review` | The claim is known to the brand; no decision yet. Common for newly self-publishing sub-brand teams whose parent hasn't reciprocated. | +| `disputed` | The brand actively rejects this claim. | +| `not_ours` | The brand affirms it is not their property. | +| `licensed_in` | This brand uses the asset under license from another entity (typed: `licensor_domain`). | +| `licensed_out` | This brand licenses the asset to another entity. | +| `unknown` | The agent has no position. Caller MAY fall back to crawl. | + +Not every tool returns every status — e.g., `verify_subsidiary_claim` doesn't return `licensed_in` / `licensed_out` (subsidiaries aren't licensed; brands and trademarks can be). The shared enum keeps callers' decoding logic uniform. + +### `verify_subsidiary_claim` + +A consumer detects a leaf-only edge: `converse.com` claims `house_domain: "nikeinc.com"`, but Nike's `brand_refs[]` doesn't include it yet. Today the consumer is told to email `contact.email` and wait. With this tool: + +```json +// Request to nikeinc.com's brand-agent +{ + "claimed_subsidiary_domain": "converse.com", + "claimed_brand_id": "converse" +} +``` + +```json +// Response +{ + "status": "pending_review", + "first_observed_by_house_at": "2026-05-12T14:00:00Z", + "expected_resolution_window_days": 7 +} +``` + +The agent's response is authoritative. The crawl backstop becomes unnecessary for the partner who has this answer. + +### `verify_property` + +A DSP shopping inventory finds a property declaration on a brand's `properties[]` and wants to confirm before bidding. Or a fraud detector found a domain claiming to be Nike's and wants the brand's say-so: + +```json +// Request +{ + "property": { + "type": "website", + "identifier": "nike.cn" + } +} +``` + +```json +// Response +{ + "status": "owned", + "relationship": "owned", + "regions": ["CN"], + "context": "Regional site for China market" +} +``` + +The `relationship` field mirrors `brand.json`'s [property relationship enum](/docs/brand-protocol/brand-json#property-relationships) (`owned`, `direct`, `delegated`, `ad_network`). A `not_ours` response carries no `relationship`. + +### `verify_trademark` + +A creative-approval workflow encounters a mark and wants to verify ownership and use: + +```json +// Request +{ + "mark": "AIR JORDAN", + "registry": "USPTO", + "number": "1234567" +} +``` + +```json +// Response +{ + "status": "owned", + "license_type": "owned", + "countries": ["US", "EU", "JP"], + "nice_classes": [25, 41] +} +``` + +For licensed marks, `status: "licensed_in"` carries `licensor_domain`. The full schema covers the same shape as `brand.json`'s [`#/definitions/trademark`](/docs/brand-protocol/brand-json#trademarks), which this tool's responses reuse where applicable. + +## Discovery and routing + +Existing AdCP convention: a brand-agent advertises its tasks via `get_adcp_capabilities`. The verification tools surface as additional task names alongside `get_brand_identity`: + +```json +{ + "supported_protocols": ["brand"], + "supported_tasks": [ + "get_brand_identity", + "verify_subsidiary_claim", + "verify_property", + "verify_trademark" + ] +} +``` + +Brand-agent implementations are free to support all, some, or none. A brand without a brand-agent (variants 1, 2, 4, 5 with no `agents[]` entry) falls back to the existing crawl-based mutual-assertion model and the `contact.email` SHOULD from PR #4505. + +When a brand-agent is advertised and the relevant tool is in `supported_tasks`, the [Conformance](/docs/brand-protocol/brand-json#conformance) clause around mutual-assertion verification SHOULD prefer the agent's authoritative answer over the crawl-based inference. The crawl path remains the default; the agent path is a discoverable upgrade. + +## Authorization and access tiers + +Verification tools follow the same public/authorized split as `get_brand_identity`: + +| Tier | What the agent returns | +|---|---| +| **Public** (no linked account) | Status enum + minimal context. "Is this mine?" gets a yes/no/pending answer. | +| **Authorized** (linked via [`sync_accounts`](/docs/accounts/tasks/sync_accounts)) | Status + richer context (registration details, regions, use case eligibility, expected resolution window for pending). | + +A consumer that is NOT authorized still gets a useful answer — the binary "is this yours" — without exposing the brand's internal queue state or licensing detail. Authorized consumers (e.g., a DSP that has linked accounts with the brand for inventory shopping) get the nuanced state. + +The `status` enum is always returned at both tiers. Optional fields (`first_observed_by_house_at`, `licensor_domain`, `expected_resolution_window_days`) are gated. + +## Trust model + +The agent's response is **authoritative**: signed under the brand's `adcp_use: "request-signing"` JWK (existing AdCP convention) and consumed under that contract. A signed `not_ours` from the brand's own agent overrides a third-party leaf's `house_domain` claim — the brand has spoken and is the authority over its own ownership. + +The crawl-based mutual-assertion model remains the fallback when: +- No brand-agent is advertised in the brand.json `agents[]` +- The advertised agent doesn't implement the relevant task +- The agent's response is `unknown` +- The agent is unreachable (transport-level failure) + +When both paths exist and the agent answers, the agent wins. + +## Internal house responsibility — the self-healing loop + +The brand-agent is responsible for what was previously the email-notification surface. When a consumer asks `verify_subsidiary_claim` and the brand has never seen the claim before, the agent: + +1. Returns `pending_review` to the caller (or `unknown` if it can't classify). +2. **Internally** logs the inbound claim, optionally notifies the house's portfolio team, and queues the claim for review. + +This is implementation-defined and not part of the protocol contract. The protocol just provides the entry point; what the brand does internally is its business. The [PR #4505 self-healing SHOULD](/docs/brand-protocol/brand-json#self-healing-through-notification) effectively migrates from "consumer emails the house" to "the agent is the house's intake." For houses without a brand-agent, the email SHOULD remains. + +## Out of scope + +Three categories the verification surface deliberately doesn't cover: + +- **Creative conflict / brand-safety scoring.** "Is this creative in conflict with our guidelines?" is its own design space — overlaps with creative protocol and brand-safety vendors. May land as a separate RFC. +- **Competitive relationships.** "Is this brand competitive to us?" is interesting but politically loaded; a brand declaring competitors machine-readably has different incentives than declaring its own assets. Deferred. +- **Cryptographic provenance** of agent-issued statements. Today: rely on transport-level signing (RFC 9421) and `adcp_use`-tagged keys. Cryptographic claim-chains (e.g., "this trademark assertion is signed by the registrar plus the brand") are out of scope. + +## Migration + +No migration burden. All three tools are additive — a brand-agent that doesn't implement them simply doesn't advertise them. Consumers that don't speak these tools fall back to the existing crawl + `contact.email` paths. + +The [Conformance](/docs/brand-protocol/brand-json#conformance) update is one sentence: "Where the named house publishes a brand-agent advertising `verify_subsidiary_claim` in `supported_tasks`, consumers SHOULD prefer the agent's response over crawl-based mutual-assertion inference." + +## Open questions + +1. **Should `verify_property` accept a `use_case` field** (advertising / endorsement / retail / etc.) that the agent can scope its answer to? E.g., a brand might own a domain but not authorize ad-network resale on it. Vote: yes, mirror the `get_brand_identity` `use_case` pattern. +2. **Should `pending_review` include the queue position** or expected resolution time as authorized-tier data, or is `expected_resolution_window_days` (an aggregate) enough? Vote: aggregate only — exposing queue position leaks brand-internal operations. +3. **What happens when a brand-agent answers `not_ours` and the leaf publishes `house_domain` pointing at the brand?** Spec says agent wins (treat leaf as having a disputed parent claim). UI guidance: surface "disputed" prominently — this is the brand telling the consumer "we reject this." Vote: confirm. +4. **Rate-limiting.** The agent decides, but the spec should set expectations. Vote: agent MAY rate-limit per `{caller_identity, tool, query_target}` pair; caller's `idempotency_key` (from `version-envelope`) lets it retry without duplicate side effects. +5. **Bulk variants** (`verify_subsidiary_claims`, plural). For a crawler verifying 100 leaves at once, a bulk API reduces round-trips. Vote: defer — single-target tools cover v1; bulk is additive when call volume justifies it. + +## References + +- [#4505](https://github.com/adcontextprotocol/adcp/pull/4505) — landed distributed brand.json + the email-based self-healing SHOULD this RFC supersedes +- [#4521](https://github.com/adcontextprotocol/adcp/issues/4521) — original "typed verification endpoint" issue that motivated this work +- [`brand.json`](/docs/brand-protocol/brand-json) — normative spec for the underlying mutual-assertion trust model +- [`building-a-brand-agent`](/docs/brand-protocol/building-a-brand-agent) — Tier 1 (`get_brand_identity`) reference; Tier 2 (this RFC) implementation guide is a planned addition +- [`get_brand_identity`](/docs/brand-protocol/tasks/get_brand_identity) — the existing tool whose pattern these verifications follow diff --git a/docs/brand-protocol/tasks/verify_property.mdx b/docs/brand-protocol/tasks/verify_property.mdx new file mode 100644 index 0000000000..2cfc8bbaa1 --- /dev/null +++ b/docs/brand-protocol/tasks/verify_property.mdx @@ -0,0 +1,116 @@ +--- +title: verify_property +description: "verify_property is the AdCP brand-protocol task for asking a brand-agent whether a property — website, app, podcast, etc. — belongs to this brand. Returns an authoritative ownership status with the commercial relationship (owned, direct, delegated, ad_network) and optional use-case authorization." +"og:title": "AdCP — verify_property" +testable: true +--- + + +**Proposed (RFC).** This task is part of the [brand verification RFC](/docs/brand-protocol/proposals/brand-verification-rfc) and not yet normative. + + +Ask a brand-agent whether a property (website, app, podcast, etc.) belongs to this brand. Used by DSPs verifying inventory ownership, fraud detectors confirming domain claims, or any consumer that wants the brand's authoritative answer rather than inferring from a crawl of `properties[]`. + +## Schema + +- **Request**: [`verify-property-request.json`](https://adcontextprotocol.org/schemas/v3/brand/verify-property-request.json) +- **Response**: [`verify-property-response.json`](https://adcontextprotocol.org/schemas/v3/brand/verify-property-response.json) + +## When to use + +A DSP about to bid on inventory associated with `nike.cn` wants to confirm Nike owns it before extending brand-safety trust. A fraud detector found a domain claiming to be Nike's and wants the brand's say-so. A creative-approval workflow encountered a property reference and needs ownership confirmation. + +The static `brand.json` `properties[]` array is the published self-declaration. `verify_property` is the **real-time authoritative** answer from the brand-agent, including the `relationship` enum (owned / direct / delegated / ad_network) that mirrors the static declaration plus optional `use_case_authorization` gating. + +## Public by default + +| Tier | What you get | +|---|---| +| **Public** | `status`, `relationship` (when owned), `brand_id`, `regions`, `context_note`. | +| **Authorized** | Everything above, plus `use_case_authorization` — per-use-case permission flags. | + +## Quick start + + +```json Request (verify a regional domain) +{ + "property": { + "type": "website", + "identifier": "nike.cn", + "region": "CN" + } +} +``` + +```json Response (owned) +{ + "status": "owned", + "relationship": "owned", + "brand_id": "nike", + "regions": ["CN"], + "context_note": "Regional site for China market" +} +``` + +```json Request (verify an app bundle) +{ + "property": { + "type": "mobile_app", + "identifier": "com.nike.snkrs", + "store": "apple" + } +} +``` + +```json Response (owned, authorized caller with use case) +{ + "status": "owned", + "relationship": "owned", + "brand_id": "jordan", + "use_case_authorization": { + "advertising": true, + "endorsement": false + } +} +``` + +```json Response (disputed) +{ + "status": "disputed", + "context_note": "Domain owned by unaffiliated third party; we do not authorize use of our marks on it." +} +``` + +```json Response (not ours) +{ + "status": "not_ours" +} +``` + + +## Relationship and use case + +The `relationship` field — present when status is `owned` — answers "the brand controls this property, but commercially how?": + +- `owned` — brand owns and operates the property directly. +- `direct` — brand is the direct sales path even if a third party runs the tech. +- `delegated` — brand has delegated monetization (e.g., Mediavine for a food blog). +- `ad_network` — brand sells as part of a network/exchange. + +The `use_case` request field — and the corresponding `use_case_authorization` response — handle the orthogonal question of "is the named use case allowed on this property?" A brand may own a domain but not authorize ad-network resale on it; the use-case gate exposes that without leaking the brand's full commercial policy. + +## Trust model + +Identical to [`verify_subsidiary_claim`](/docs/brand-protocol/tasks/verify_subsidiary_claim#trust-model). Signed authoritative response from the agent overrides static `brand.json` `properties[]` inference. When the agent is unreachable or returns `unknown`, fall back to crawl-based inference from the static file. + +## Caching + +Properties churn less than subsidiary claims. Cache-friendly: + +- `owned` / `not_ours` — stable. 24-72h cache. +- `disputed` — stable until disputed party changes claim. 24h cache. +- `use_case_authorization` — MAY be more volatile (a brand might rotate ad-network authorization). Re-check per session is reasonable. + +## Error handling + +See [`verify_subsidiary_claim` § Error handling](/docs/brand-protocol/tasks/verify_subsidiary_claim#error-handling). Same error code surface applies. diff --git a/docs/brand-protocol/tasks/verify_subsidiary_claim.mdx b/docs/brand-protocol/tasks/verify_subsidiary_claim.mdx new file mode 100644 index 0000000000..c5576c8a90 --- /dev/null +++ b/docs/brand-protocol/tasks/verify_subsidiary_claim.mdx @@ -0,0 +1,121 @@ +--- +title: verify_subsidiary_claim +description: "verify_subsidiary_claim is the AdCP brand-protocol task for asking a brand-agent whether a named brand is a subsidiary of this house. Returns an authoritative status — owned, pending_review, disputed, not_ours, or unknown — used in place of crawl-based mutual-assertion inference when the brand-agent supports it." +"og:title": "AdCP — verify_subsidiary_claim" +testable: true +--- + + +**Proposed (RFC).** This task is part of the [brand verification RFC](/docs/brand-protocol/proposals/brand-verification-rfc) and not yet normative. The crawl-based [mutual-assertion model](/docs/brand-protocol/brand-json#mutual-assertion-trust-model) remains authoritative until the RFC ratifies. + + +Ask a brand-agent whether the named brand is a subsidiary of this house. Returns an authoritative status from the brand's own agent, replacing crawl-based inference when the brand-agent supports this task. + +## Schema + +- **Request**: [`verify-subsidiary-claim-request.json`](https://adcontextprotocol.org/schemas/v3/brand/verify-subsidiary-claim-request.json) +- **Response**: [`verify-subsidiary-claim-response.json`](https://adcontextprotocol.org/schemas/v3/brand/verify-subsidiary-claim-response.json) + +## When to use + +The crawl-based mutual-assertion model — leaf publishes `house_domain: X`, X's `brand_refs[]` includes leaf, both halves verified by independent fetch — covers the common case. But it has known gaps: + +- **Two-state visibility.** Crawl answers "mutual" or "not mutual." It can't surface *pending review* or *disputed*. +- **TTL-bound freshness.** A consumer's view is only as fresh as its last crawl of both sides. +- **One-sided pessimism.** A leaf-only edge gets downgraded to "claimed, unverified" even when the brand intends to confirm. + +`verify_subsidiary_claim` asks the brand directly. A signed `owned` from the brand's own agent is stronger than a published `brand_refs[]` entry (since it's real-time). A signed `not_ours` overrides a leaf's `house_domain` claim. And `pending_review` surfaces a state the crawl model simply can't. + +## Public by default + +Every brand-agent that implements this task MUST return a `status` to any caller. Authorized callers (linked via [`sync_accounts`](/docs/accounts/tasks/sync_accounts)) additionally receive richer context — `first_observed_by_house_at`, `expected_resolution_window_days`, and the brand's `brand_id` for the subsidiary. + +| Tier | What you get | +|---|---| +| **Public** | `status`, public `brand_id` (when applicable), public `dispute_reason` (when status is `disputed`). | +| **Authorized** | Everything above, plus `first_observed_by_house_at` and `expected_resolution_window_days`. | + +Queue position, internal ticket state, and team routing are NEVER exposed — neither to the public nor to authorized callers. + +## Quick start + + +```json Request +{ + "claimed_subsidiary_domain": "converse.com", + "claimed_brand_id": "converse", + "observed_at": "2026-05-14T10:00:00Z" +} +``` + +```json Response (mutual) +{ + "status": "owned", + "brand_id": "converse" +} +``` + +```json Response (pending, authorized caller) +{ + "status": "pending_review", + "brand_id": "converse", + "first_observed_by_house_at": "2026-05-12T14:00:00Z", + "expected_resolution_window_days": 7 +} +``` + +```json Response (disputed) +{ + "status": "disputed", + "dispute_reason": "We do not own this brand; trademark held by separate entity." +} +``` + +```json Response (not ours) +{ + "status": "not_ours" +} +``` + +```json Response (unknown) +{ + "status": "unknown" +} +``` + + +## Trust model + +The agent's response is **authoritative** when signed under the brand's `adcp_use: "request-signing"` JWK. Consumers SHOULD prefer the agent's response over crawl-based mutual-assertion inference when the brand-agent advertises `verify_subsidiary_claim` in its `supported_tasks`. + +Conflict resolution between agent response and crawl observation: + +| Agent says | Crawl says | Consumer treats as | +|---|---|---| +| `owned` | Mutual or leaf-only | Trusted edge. Governance propagation: yes. | +| `pending_review` | Leaf-only (parent silent) | Trusted leaf identity; relationship trust withheld. UI: "Pending parent confirmation, expected within N days." | +| `disputed` | Leaf-only or mutual | The brand has spoken. Treat leaf as having a rejected parent claim. UI: "Brand X says this is not theirs." | +| `not_ours` | Leaf-only | Leaf is misclassified; treat as standalone with disputed parent claim. | +| `unknown` | Any | Fall back to crawl-based mutual-assertion inference. | + +When the agent answers, the agent wins. The crawl path remains as fallback only when the agent is unreachable, `unknown`, or doesn't implement the task. + +## Caching + +Brand-agent responses to this task SHOULD carry standard HTTP cache headers and a TTL appropriate to the volatility: + +- `owned` / `not_ours` — stable. 24h cache is reasonable. +- `pending_review` — volatile. Re-check on each governance decision. +- `disputed` — stable until disputed party publishes new claim. 24h cache is reasonable. + +The agent MAY return `Cache-Control: max-age=N` to constrain consumer caching. + +## Error handling + +Tasks return the `VerifySubsidiaryClaimError` arm on transport/auth failures: + +- `AUTH_INVALID` — caller's signed envelope did not verify. (Distinct from `AUTH_MISSING`: an unauthenticated public call returns the public success arm with `status`, not an error.) +- `RATE_LIMITED` — agent has rate-limited the caller (typically per `{caller_identity, claimed_subsidiary_domain}`). +- `INVALID_INPUT` — `claimed_subsidiary_domain` is not a valid hostname or fails the agent's input policy. + +Operational failures (the agent's internal database is down, etc.) MAY return `status: "unknown"` rather than an error — the caller can fall back to crawl. diff --git a/docs/brand-protocol/tasks/verify_trademark.mdx b/docs/brand-protocol/tasks/verify_trademark.mdx new file mode 100644 index 0000000000..26b42c4b84 --- /dev/null +++ b/docs/brand-protocol/tasks/verify_trademark.mdx @@ -0,0 +1,145 @@ +--- +title: verify_trademark +description: "verify_trademark is the AdCP brand-protocol task for asking a brand-agent whether a trademark is owned, licensed in, or licensed out by this brand. Returns registration details, jurisdictions, Nice classes, and optional per-use-case authorization." +"og:title": "AdCP — verify_trademark" +testable: true +--- + + +**Proposed (RFC).** This task is part of the [brand verification RFC](/docs/brand-protocol/proposals/brand-verification-rfc) and not yet normative. + + +Ask a brand-agent whether a trademark is owned, licensed in, or licensed out by this brand. Used in creative approval workflows, brand-safety pipelines, and any consumer that needs the brand's authoritative ownership statement over a mark. + +## Schema + +- **Request**: [`verify-trademark-request.json`](https://adcontextprotocol.org/schemas/v3/brand/verify-trademark-request.json) +- **Response**: [`verify-trademark-response.json`](https://adcontextprotocol.org/schemas/v3/brand/verify-trademark-response.json) + +## When to use + +Trademark ownership has gnarly edges that static publication can't always resolve: + +- **Cross-jurisdiction conflicts.** USPTO `CONVERSE` and EUIPO `CONVERSE` may be held by different parties. +- **Nice class disambiguation.** `DELTA` (airline, class 39) and `DELTA` (faucet, class 11) are different brands with the same mark. +- **Licensing chains.** A brand uses a mark under license (`licensed_in`) or licenses it out (`licensed_out`) — neither is captured cleanly by static publication alone. + +`verify_trademark` lets a consumer ask the brand's authoritative agent for a definitive answer, including the matched registration and the licensing relationship. + +## Public by default + +| Tier | What you get | +|---|---| +| **Public** | `status`, `matched_registration`, `license_type`, `licensor_domain` (when `licensed_in`), `countries`, `nice_classes`, `context_note`. | +| **Authorized** | Everything above, plus `use_case_authorization` — per-use-case permission flags. | + +Trademark facts are largely public-record (registry filings are open). The agent surfaces this for callers who don't want to crawl every registry; authorized callers additionally learn which use cases the brand authorizes for the mark. + +## Quick start + + +```json Request (verify by registry number) +{ + "mark": "AIR JORDAN", + "registry": "USPTO", + "number": "1234567" +} +``` + +```json Response (owned) +{ + "status": "owned", + "matched_registration": { + "registry": "USPTO", + "number": "1234567", + "mark": "AIR JORDAN", + "status": "active" + }, + "license_type": "owned", + "countries": ["US"], + "nice_classes": [25, 41] +} +``` + +```json Request (verify by mark + region) +{ + "mark": "CONVERSE", + "countries": ["EU"] +} +``` + +```json Response (licensed_in) +{ + "status": "licensed_in", + "matched_registration": { + "registry": "EUIPO", + "number": "EU98765", + "mark": "CONVERSE", + "status": "active" + }, + "license_type": "licensed_in", + "licensor_domain": "converseholdings-eu.com", + "countries": ["FR", "DE", "IT", "ES"], + "nice_classes": [25] +} +``` + +```json Response (authorized, with use cases) +{ + "status": "owned", + "matched_registration": { "registry": "USPTO", "number": "9999999", "mark": "SWOOSH", "status": "active" }, + "license_type": "owned", + "countries": ["US"], + "nice_classes": [25, 41], + "use_case_authorization": { + "editorial": true, + "commercial_advertising": false, + "merchandise_resale": false + } +} +``` + +```json Response (disputed) +{ + "status": "disputed", + "context_note": "Mark in this jurisdiction held by separate entity; we contest their registration." +} +``` + +```json Response (not ours) +{ + "status": "not_ours" +} +``` + + +## Status semantics for trademarks + +| Status | Meaning | +|---|---| +| `owned` | The brand holds the registration. | +| `licensed_in` | The brand uses the mark under license from another entity (`licensor_domain` populated). | +| `licensed_out` | The brand licenses the mark to another entity. | +| `disputed` | The brand actively contests this registration (e.g., another party registered the mark in a jurisdiction the brand intends to challenge). | +| `not_ours` | The brand has no relationship to this registration. | +| `unknown` | The agent cannot match the input to any registration. Caller MAY fall back to public registry crawls. | + +`pending_review` is uncommon for trademarks — registration is a public-record event with definitive ownership at any given time. Agents MAY return it for marks under M&A or licensing transition. + +## Trust model + +Identical to [`verify_subsidiary_claim`](/docs/brand-protocol/tasks/verify_subsidiary_claim#trust-model). The agent's signed response is authoritative; crawl of public trademark registries is the fallback. When the agent and registry disagree (rare but real — e.g., a mark recently transferred but not yet reflected in the registry's public records), the agent wins because the brand is the authority over its current licensing relationships. + +## Caching + +Trademark registration data is stable; licensing relationships less so: + +- `owned` / `not_ours` / `disputed` — stable. 24-72h cache. +- `licensed_in` / `licensed_out` — moderately volatile. 24h cache; re-check on contract-affected workflows. +- `use_case_authorization` — most volatile field. Re-check per session. + +## Error handling + +See [`verify_subsidiary_claim` § Error handling](/docs/brand-protocol/tasks/verify_subsidiary_claim#error-handling). Additional error codes specific to trademarks: + +- `AMBIGUOUS_MATCH` — multiple registrations match the input (typically when neither `registry` nor `number` is provided and the mark exists in several jurisdictions). Caller should narrow the query with `registry`, `number`, or `countries`. diff --git a/static/schemas/source/brand/verification-status.json b/static/schemas/source/brand/verification-status.json new file mode 100644 index 0000000000..6d0f1d4e5b --- /dev/null +++ b/static/schemas/source/brand/verification-status.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/brand/verification-status.json", + "title": "Verification Status", + "description": "Status returned by brand-agent verification tools (verify_subsidiary_claim, verify_property, verify_trademark). Captures the rich state surface crawl-based mutual-assertion cannot express: pending review, disputed, licensed in/out, and explicit not-ours / unknown answers. Not every tool returns every status — see each tool's response schema for the applicable subset.", + "type": "string", + "enum": [ + "owned", + "pending_review", + "disputed", + "not_ours", + "licensed_in", + "licensed_out", + "unknown" + ], + "enumDescriptions": { + "owned": "Definitively belongs to this brand, currently.", + "pending_review": "The brand is aware of this claim and has not yet decided. Consumers SHOULD NOT extend governance trust through pending claims; treating them as 'will-likely-be-confirmed' is also wrong — they may still be disputed.", + "disputed": "The brand actively rejects this claim. Consumers MUST treat the claim as invalid and SHOULD surface the dispute (e.g., 'X says this is not theirs').", + "not_ours": "The brand affirms it is not their property / subsidiary / mark. Equivalent to 'disputed' but used when the brand-agent has no record of an existing claim — a clean 'we do not own this.'", + "licensed_in": "The brand uses this asset under license from another entity. The response carries `licensor_domain` when this status is returned. Applies to trademarks and (rarely) properties.", + "licensed_out": "The brand licenses this asset to another entity. Applies to trademarks; rare for properties or subsidiaries.", + "unknown": "The agent has no position. Caller MAY fall back to crawl-based mutual-assertion inference. Returned when the agent cannot classify the input (insufficient context, unrecognized identifier, out-of-scope query)." + } +} diff --git a/static/schemas/source/brand/verify-property-request.json b/static/schemas/source/brand/verify-property-request.json new file mode 100644 index 0000000000..d16be7cd31 --- /dev/null +++ b/static/schemas/source/brand/verify-property-request.json @@ -0,0 +1,47 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/brand/verify-property-request.json", + "title": "Verify Property Request", + "description": "Ask a brand-agent whether a property (website, app, podcast, etc.) belongs to this brand. Used by DSPs verifying inventory ownership, fraud detectors confirming domain claims, or any consumer that wants the brand's authoritative answer rather than inferring from a crawl of properties[]. Read-only; naturally idempotent.", + "type": "object", + "allOf": [ + { + "$ref": "/schemas/core/version-envelope.json" + } + ], + "properties": { + "property": { + "type": "object", + "description": "The property whose ownership is being verified. Shape matches brand.json's properties[] entry.", + "properties": { + "type": { + "type": "string", + "enum": ["website", "mobile_app", "ctv_app", "desktop_app", "dooh", "podcast", "radio", "streaming_audio"], + "description": "Property type, matching the brand.json properties[] enum." + }, + "identifier": { + "type": "string", + "description": "Property identifier — domain for website/podcast/radio, bundle id for apps, etc." + }, + "store": { + "type": "string", + "enum": ["apple", "google", "amazon", "roku", "fire_tv", "samsung", "lg", "vizio", "other"], + "description": "App store, when property type is an app." + }, + "region": { + "type": "string", + "description": "ISO 3166-1 alpha-2 country code or 'global'." + } + }, + "required": ["type", "identifier"], + "additionalProperties": true + }, + "use_case": { + "type": "string", + "description": "Optional caller-declared use case (e.g., 'advertising', 'endorsement', 'retail_listing'). The agent MAY scope its answer to this use case — e.g., a brand may own a domain but not authorize ad-network resale on it.", + "maxLength": 100 + } + }, + "required": ["property"], + "additionalProperties": true +} diff --git a/static/schemas/source/brand/verify-property-response.json b/static/schemas/source/brand/verify-property-response.json new file mode 100644 index 0000000000..9eca8b39cf --- /dev/null +++ b/static/schemas/source/brand/verify-property-response.json @@ -0,0 +1,87 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/brand/verify-property-response.json", + "title": "Verify Property Response", + "description": "The brand-agent's authoritative answer on property ownership. Mirrors the property-relationship enum used in brand.json properties[].", + "type": "object", + "allOf": [ + { + "$ref": "/schemas/core/version-envelope.json" + } + ], + "oneOf": [ + { + "title": "VerifyPropertySuccess", + "properties": { + "status": { + "$ref": "/schemas/brand/verification-status.json", + "description": "Verification status. Applicable values for this tool: owned, disputed, not_ours, unknown. (pending_review and licensed_in/out are uncommon for properties; agents MAY return them but most properties have a definitive yes/no answer.)" + }, + "relationship": { + "type": "string", + "enum": ["owned", "direct", "delegated", "ad_network"], + "description": "Public — when status is `owned`, the commercial relationship between the brand and the property. Mirrors brand.json's properties[].relationship enum. Omitted when status is not `owned`." + }, + "brand_id": { + "type": "string", + "description": "Public — the brand_id within the house this property belongs to. Lets the caller cross-reference with brand.json data.", + "pattern": "^[a-z0-9_]+$" + }, + "regions": { + "type": "array", + "items": { "type": "string" }, + "description": "Public — ISO 3166-1 alpha-2 country codes where this property is the brand's primary/canonical surface. Empty array = global / no regional restriction." + }, + "use_case_authorization": { + "type": "object", + "description": "Authorized-tier only. Per-use-case authorization flags. The agent's answer to whether the named use case is permitted on this property — distinct from ownership.", + "properties": { + "advertising": { "type": "boolean", "description": "Programmatic advertising allowed via AdCP." }, + "endorsement": { "type": "boolean", "description": "Use of the property in endorsement deals." }, + "retail_listing": { "type": "boolean", "description": "Listing in retail / commerce contexts." } + }, + "additionalProperties": { "type": "boolean" } + }, + "context_note": { + "type": "string", + "maxLength": 500, + "description": "Public — free-text context the brand chooses to surface (e.g., 'Regional CN site', 'Legacy domain, redirects to nike.com')." + }, + "context": { + "$ref": "/schemas/core/context.json" + }, + "ext": { + "$ref": "/schemas/core/ext.json" + } + }, + "required": ["status"], + "additionalProperties": true + }, + { + "title": "VerifyPropertyError", + "properties": { + "errors": { + "type": "array", + "items": { + "$ref": "/schemas/core/error.json" + }, + "minItems": 1 + }, + "context": { + "$ref": "/schemas/core/context.json" + }, + "ext": { + "$ref": "/schemas/core/ext.json" + } + }, + "required": ["errors"], + "additionalProperties": true, + "not": { + "anyOf": [ + { "required": ["status"] } + ] + } + } + ], + "properties": {} +} diff --git a/static/schemas/source/brand/verify-subsidiary-claim-request.json b/static/schemas/source/brand/verify-subsidiary-claim-request.json new file mode 100644 index 0000000000..5503dc1c96 --- /dev/null +++ b/static/schemas/source/brand/verify-subsidiary-claim-request.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/brand/verify-subsidiary-claim-request.json", + "title": "Verify Subsidiary Claim Request", + "description": "Ask a brand-agent whether the named brand is a subsidiary of this house. Used by consumers (DSPs, crawlers, partner agents) that have detected a leaf claiming `house_domain` pointing at this brand and want an authoritative answer before extending governance trust. The crawl-based mutual-assertion path remains the fallback when the brand-agent doesn't implement this task or returns `unknown`. Read-only; naturally idempotent — the brand's internal claim-bookkeeping deduplicates on (caller_identity, claimed_subsidiary_domain).", + "type": "object", + "allOf": [ + { + "$ref": "/schemas/core/version-envelope.json" + } + ], + "properties": { + "claimed_subsidiary_domain": { + "type": "string", + "description": "The domain of the leaf brand whose `house_domain` claim is being verified. Typically the leaf's canonical brand.json is hosted at this domain (or via authoritative_location indirection).", + "format": "hostname" + }, + "claimed_brand_id": { + "type": "string", + "description": "Stable brand identifier the leaf uses for itself. Optional but recommended — lets the agent disambiguate when multiple brands share a domain (rare but legal).", + "pattern": "^[a-z0-9_]+$" + }, + "observed_at": { + "type": "string", + "format": "date-time", + "description": "When the caller observed the leaf's claim. Helps the agent age claims and prioritize fresh ones in its internal queue." + } + }, + "required": ["claimed_subsidiary_domain"], + "additionalProperties": true +} diff --git a/static/schemas/source/brand/verify-subsidiary-claim-response.json b/static/schemas/source/brand/verify-subsidiary-claim-response.json new file mode 100644 index 0000000000..12e350744c --- /dev/null +++ b/static/schemas/source/brand/verify-subsidiary-claim-response.json @@ -0,0 +1,77 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/brand/verify-subsidiary-claim-response.json", + "title": "Verify Subsidiary Claim Response", + "description": "The brand-agent's authoritative answer to whether the claimed brand is a subsidiary of this house. Always returns a `status`; richer fields are gated by caller authorization.", + "type": "object", + "allOf": [ + { + "$ref": "/schemas/core/version-envelope.json" + } + ], + "oneOf": [ + { + "title": "VerifySubsidiaryClaimSuccess", + "properties": { + "status": { + "$ref": "/schemas/brand/verification-status.json", + "description": "Verification status. Applicable values for this tool: owned, pending_review, disputed, not_ours, unknown. (licensed_in / licensed_out do not apply to subsidiaries.)" + }, + "brand_id": { + "type": "string", + "description": "Public — the house's brand_id for this subsidiary when status is `owned` or `pending_review`. Lets the caller cross-reference with brand_refs[] / brands[] entries.", + "pattern": "^[a-z0-9_]+$" + }, + "first_observed_by_house_at": { + "type": "string", + "format": "date-time", + "description": "Authorized-tier only. When the house first became aware of this claim (whether via this RPC, an inbound notification, or its own discovery)." + }, + "expected_resolution_window_days": { + "type": "integer", + "minimum": 0, + "description": "Authorized-tier only. The house's expected window for resolving a `pending_review` claim. Aggregate signal, not a guarantee; does not expose queue position or internal operations." + }, + "dispute_reason": { + "type": "string", + "maxLength": 500, + "description": "Public — when status is `disputed`, a short human-readable rationale (e.g., 'Trademark conflict; not affiliated'). May be empty even on disputed." + }, + "context": { + "$ref": "/schemas/core/context.json" + }, + "ext": { + "$ref": "/schemas/core/ext.json" + } + }, + "required": ["status"], + "additionalProperties": true + }, + { + "title": "VerifySubsidiaryClaimError", + "properties": { + "errors": { + "type": "array", + "items": { + "$ref": "/schemas/core/error.json" + }, + "minItems": 1 + }, + "context": { + "$ref": "/schemas/core/context.json" + }, + "ext": { + "$ref": "/schemas/core/ext.json" + } + }, + "required": ["errors"], + "additionalProperties": true, + "not": { + "anyOf": [ + { "required": ["status"] } + ] + } + } + ], + "properties": {} +} diff --git a/static/schemas/source/brand/verify-trademark-request.json b/static/schemas/source/brand/verify-trademark-request.json new file mode 100644 index 0000000000..d922633a61 --- /dev/null +++ b/static/schemas/source/brand/verify-trademark-request.json @@ -0,0 +1,42 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/brand/verify-trademark-request.json", + "title": "Verify Trademark Request", + "description": "Ask a brand-agent whether a trademark is owned, licensed in, or licensed out by this brand. Used in creative approval workflows, brand-safety pipelines, and any consumer that needs the brand's authoritative ownership statement over a mark. Read-only; naturally idempotent.", + "type": "object", + "allOf": [ + { + "$ref": "/schemas/core/version-envelope.json" + } + ], + "properties": { + "mark": { + "type": "string", + "description": "The registered mark as published (e.g., 'AIR JORDAN', 'NIKE', 'SWOOSH'). At minimum, callers must provide the mark text; registry and number narrow the query when multiple registrations exist.", + "minLength": 1, + "maxLength": 200 + }, + "registry": { + "type": "string", + "description": "Trademark registry (e.g., 'USPTO', 'EUIPO', 'JPO', 'CNIPA'). Strongly recommended when the same mark is registered separately by different parties across jurisdictions.", + "maxLength": 50 + }, + "number": { + "type": "string", + "description": "Registration number as issued by the registry. Most precise narrowing key; agents will use this when present.", + "maxLength": 100 + }, + "countries": { + "type": "array", + "items": { "type": "string", "minLength": 2, "maxLength": 2 }, + "description": "Optional ISO 3166-1 alpha-2 country codes scoping the query. If a brand owns a mark in some jurisdictions and not others, this lets the caller ask about the specific region." + }, + "use_case": { + "type": "string", + "description": "Optional caller-declared use case. The agent MAY scope its answer (e.g., a brand may permit editorial use of a mark but not commercial use).", + "maxLength": 100 + } + }, + "required": ["mark"], + "additionalProperties": true +} diff --git a/static/schemas/source/brand/verify-trademark-response.json b/static/schemas/source/brand/verify-trademark-response.json new file mode 100644 index 0000000000..03d4b1342b --- /dev/null +++ b/static/schemas/source/brand/verify-trademark-response.json @@ -0,0 +1,102 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/brand/verify-trademark-response.json", + "title": "Verify Trademark Response", + "description": "The brand-agent's authoritative answer on trademark ownership and licensing. Mirrors the shape of brand.json's #/definitions/trademark where status is `owned`, `licensed_in`, or `licensed_out`.", + "type": "object", + "allOf": [ + { + "$ref": "/schemas/core/version-envelope.json" + } + ], + "oneOf": [ + { + "title": "VerifyTrademarkSuccess", + "properties": { + "status": { + "$ref": "/schemas/brand/verification-status.json", + "description": "Verification status. Applicable values for this tool: owned, licensed_in, licensed_out, disputed, not_ours, unknown. (pending_review is uncommon — trademark registrations are public-record events with definitive ownership at any given time.)" + }, + "matched_registration": { + "type": "object", + "description": "Public — when status is `owned`, `licensed_in`, or `licensed_out`, the registration the agent matched the query to. Same shape as brand.json's #/definitions/trademark.", + "properties": { + "registry": { "type": "string" }, + "number": { "type": "string" }, + "mark": { "type": "string" }, + "status": { + "type": "string", + "enum": ["active", "pending", "abandoned", "cancelled", "expired"], + "description": "Registration status from the registry, distinct from this response's outer `status` field." + } + }, + "additionalProperties": true + }, + "license_type": { + "type": "string", + "enum": ["owned", "licensed_in", "licensed_out"], + "description": "Public — licensing relationship when status is `owned`, `licensed_in`, or `licensed_out`. Redundant with status for owned/licensed_in/licensed_out; kept for parity with brand.json #/definitions/trademark." + }, + "licensor_domain": { + "type": "string", + "format": "hostname", + "description": "Public — when status is `licensed_in`, the domain of the entity licensing this mark to the brand." + }, + "countries": { + "type": "array", + "items": { "type": "string", "minLength": 2, "maxLength": 2 }, + "description": "Public — ISO 3166-1 alpha-2 country codes the response covers." + }, + "nice_classes": { + "type": "array", + "items": { "type": "integer", "minimum": 1, "maximum": 45 }, + "description": "Public — Nice Classification class numbers covered. Disambiguates cross-industry marks (Delta-airline class 39 vs Delta-faucet class 11)." + }, + "use_case_authorization": { + "type": "object", + "description": "Authorized-tier only. Per-use-case permission for this mark.", + "additionalProperties": { "type": "boolean" } + }, + "context_note": { + "type": "string", + "maxLength": 500, + "description": "Public — free-text context the brand chooses to surface." + }, + "context": { + "$ref": "/schemas/core/context.json" + }, + "ext": { + "$ref": "/schemas/core/ext.json" + } + }, + "required": ["status"], + "additionalProperties": true + }, + { + "title": "VerifyTrademarkError", + "properties": { + "errors": { + "type": "array", + "items": { + "$ref": "/schemas/core/error.json" + }, + "minItems": 1 + }, + "context": { + "$ref": "/schemas/core/context.json" + }, + "ext": { + "$ref": "/schemas/core/ext.json" + } + }, + "required": ["errors"], + "additionalProperties": true, + "not": { + "anyOf": [ + { "required": ["status"] } + ] + } + } + ], + "properties": {} +} From 29d643502ca160ecfdfa70014e0e542349ef276f Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Thu, 14 May 2026 10:02:57 -0400 Subject: [PATCH 2/5] =?UTF-8?q?docs(brand-protocol):=20RFC=20v2=20?= =?UTF-8?q?=E2=80=94=20expert-review=20fixes=20across=20protocol/product/d?= =?UTF-8?q?ocs=20(#4540)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address all blockers and strong-should-fix items from the three-expert review on PR #4540. Open questions resolved with consensus positions baked into the contract. Schemas - Fix oneOf discriminator: success arm now carries symmetric not.errors, matching the error arm. Mirrors search-brands-response.json convention. - Per-tool status enum subsets — verify_property excludes pending_review and licensed_in/out; verify_trademark excludes pending_review; verify_subsidiary excludes licensed_in/out. Each tool's response constrains the shared VerificationStatus to its applicable set. - Add `transferring` to VerificationStatus — M&A in flight is distinct from pending_review. - Drop redundant `license_type` from verify-trademark-response (status already carries the licensing relationship). - Field-name consistency: `context_note` everywhere (verify_subsidiary previously had `dispute_reason`). - `regions` empty-array ambiguity resolved with `"global"` sentinel matching request schema. - Use-case authorization gains a registered starter set (advertising, endorsement, retail_listing, editorial, commercial_advertising, merchandise_resale) with additionalProperties: boolean for extensions. - pending_review now REQUIRES expected_resolution_window_days; agents MUST transition or flip to unknown past the window. RFC doc - Conflict resolution promoted from open question to normative trust model. Full table covering all status × crawl-observation combinations. - Signing key corrected: adcp_use: "response-signing" (not request-signing). Per the keys-per-purpose convention. - "DRM-shaped" stays as analogy; lead framing is "federated authoritative verification" — what the spec audience actually wants. - Motivation honest about what the surface does NOT fix: the sub-brand self-publishing problem is relocated, not solved. Partner gets typed state instead of silence; brand-side workflow gap remains. - verify_property reframed: not a bid-time tool (sub-100ms budgets); inventory onboarding / supply-path curation / fraud / clearance. - verify_trademark differentiator foregrounded: registries can't tell you authorized use cases or licensee posture. - Deployment model section added: AAO-hosted as managed service for most members; self-hosted for holdcos/agencies with bandwidth; no brand-agent as graceful-degradation path. - End-to-end Nike worked example added — six steps from discovery through full portfolio verification with cache guidance. - Caching as normative SHOULD per-status (was prose). - Rate-limiting as normative expectation: Retry-After header, prefer-cached-prior over hard error. - Prior art expanded: ads.txt for inventory authority (complementary), WHOIS/RDAP for domain registration (different layer), trademark registries (registration facts only). No direct equivalent today. - check_competitive_relationship moved to "indefinitely deferred" (politically loaded; brands won't publicly enumerate enemies). - Authorization-tier table promoted to single authoritative source; task pages refer back. - Resolved-decisions section replaces Open Questions. Task pages - Parallelism fixed for RAG retrieval: each page inlines core content (trust model row-table, caching, error codes) rather than cross- referencing back to verify_subsidiary_claim. Cross-links remain for full normative detail. - Each page adds Capability discovery snippet showing get_adcp_capabilities advertisement. - Each page's CodeGroup includes a Response (error) example. - verify_property "When to use" reframed away from bid-time. - verify_trademark leads with the differentiator vs registries. Nav and Conformance - docs.json: new Proposals group under brand-protocol nav surfaces the RFC. - brand-json Conformance bullet updated to cite the correct signing key (response-signing) and call out that signed disputed/not_ours overrides leaf-side house_domain claims. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs.json | 6 + docs/brand-protocol/brand-json.mdx | 2 +- .../proposals/brand-verification-rfc.mdx | 214 ++++++++++++++---- docs/brand-protocol/tasks/verify_property.mdx | 85 +++++-- .../tasks/verify_subsidiary_claim.mdx | 93 +++++--- .../brand-protocol/tasks/verify_trademark.mdx | 85 ++++--- .../source/brand/verification-status.json | 4 +- .../brand/verify-property-response.json | 25 +- .../verify-subsidiary-claim-response.json | 20 +- .../brand/verify-trademark-response.json | 35 +-- 10 files changed, 417 insertions(+), 152 deletions(-) diff --git a/docs.json b/docs.json index a9da76db80..4614e1aa24 100644 --- a/docs.json +++ b/docs.json @@ -505,6 +505,12 @@ "docs/brand-protocol/tasks/acquire_rights", "docs/brand-protocol/tasks/update_rights" ] + }, + { + "group": "Proposals", + "pages": [ + "docs/brand-protocol/proposals/brand-verification-rfc" + ] } ] }, diff --git a/docs/brand-protocol/brand-json.mdx b/docs/brand-protocol/brand-json.mdx index 51b83a5985..0b2854701c 100644 --- a/docs/brand-protocol/brand-json.mdx +++ b/docs/brand-protocol/brand-json.mdx @@ -1183,7 +1183,7 @@ These invariants MUST be enforced by validators and crawlers; JSON Schema cannot **Self-healing** - **Leaf-only edges SHOULD trigger notification.** When a consumer encounters a leaf claiming `house_domain: X` with no reciprocal entry on X, the consumer SHOULD notify X's `contact.email` so the house team can complete the reciprocation. Notifying parties MUST rate-limit per `{leaf, house}` pair to avoid flooding. -- **Agent-preferred verification (proposed — see [brand verification RFC](/docs/brand-protocol/proposals/brand-verification-rfc)).** When the named house publishes a brand-agent advertising `verify_subsidiary_claim` (or `verify_property` / `verify_trademark`) in its `supported_tasks`, consumers SHOULD prefer the agent's authoritative response over crawl-based mutual-assertion inference. The agent's signed response wins; the crawl path remains as fallback when the agent is unreachable or returns `unknown`. Not yet normative; the email notification SHOULD continues to apply. +- **Agent-preferred verification (proposed — see [brand verification RFC](/docs/brand-protocol/proposals/brand-verification-rfc)).** When the named house publishes a brand-agent advertising `verify_subsidiary_claim` (or `verify_property` / `verify_trademark`) in its `supported_tasks`, consumers SHOULD prefer the agent's authoritative response over crawl-based mutual-assertion inference. The agent's response is signed under the brand's `adcp_use: "response-signing"` JWK; signed `disputed` or `not_ours` from the agent overrides any leaf-side `house_domain` claim. The crawl path remains as fallback when the agent is unreachable or returns `unknown`. Not yet normative; the email notification SHOULD continues to apply for houses without a brand-agent. ## Prior art diff --git a/docs/brand-protocol/proposals/brand-verification-rfc.mdx b/docs/brand-protocol/proposals/brand-verification-rfc.mdx index f38da630a0..f8752038f2 100644 --- a/docs/brand-protocol/proposals/brand-verification-rfc.mdx +++ b/docs/brand-protocol/proposals/brand-verification-rfc.mdx @@ -1,6 +1,6 @@ --- title: "RFC — Brand verification surface" -description: "Proposal for a federated trust surface on the brand-agent. Three interrogative tools — verify_subsidiary_claim, verify_property, verify_trademark — let partners ask the brand authoritatively whether something belongs to it." +description: "Proposal for a federated authoritative verification surface on the brand-agent. Three interrogative tools — verify_subsidiary_claim, verify_property, verify_trademark — let partners ask the brand authoritatively whether something belongs to it." "og:title": "AdCP — Brand verification RFC" --- @@ -16,22 +16,24 @@ Add three interrogative tools to the brand-protocol surface that let partners as - **`verify_property`** — "Is this site / app / property actually one of yours?" - **`verify_trademark`** — "Is this trademark one of yours?" -These collapse the original [self-healing notification](/docs/brand-protocol/brand-json#self-healing-through-notification) surface (PR #4505 landed an email-based SHOULD) into a richer **pull-based federated trust** capability. A consumer (DSP, crawler, partner agent) detecting a leaf-only edge no longer pushes a notification at the house and waits — it asks the brand's agent directly and gets an authoritative answer. +These supersede the email-based [self-healing notification](/docs/brand-protocol/brand-json#self-healing-through-notification) that PR #4505 landed (the email SHOULD remains as a fallback when no brand-agent is advertised). A consumer (DSP, crawler, partner agent) detecting a leaf-only edge no longer pushes a notification at the house and waits — it asks the brand's agent directly and gets an authoritative answer. -The mutual-assertion crawl model stays the decentralized backstop. The brand-agent verification path is the federated alternative for brands that opt in. +The mutual-assertion crawl model stays the decentralized backstop. The brand-agent verification path is the federated authoritative alternative for brands that opt in. + +The metaphor is **DRM-shaped for brand identity**: the brand owns its own answers to "what's mine," and partners ask before they act. It's not gating access to public data — the data is still public — it's providing nuanced, authoritative answers that crawling cannot. The substance is **federated authoritative verification**; DRM is the analogy, not the framing for spec readers. ## Motivation `brand.json`'s mutual-assertion model is decentralized — two parties publish their halves at well-known URLs and a consumer crawls both to confirm. It's the right primitive for the open web, but it has known gaps: -- **Two-state visibility.** Crawling answers "mutual or not." It can't surface *pending review*, *disputed*, *licensed in*, or *under M&A*. Brands need a richer state surface. +- **Two-state visibility.** Crawling answers "mutual or not." It can't surface *pending review*, *transferring* (M&A in flight), *disputed*, or *licensed in*. Brands need a richer state surface. - **TTL-bound freshness.** A consumer's view is only as fresh as its last crawl. M&A-driven changes don't propagate until the next refresh. - **One-sided pessimism.** When a leaf claims a parent and the parent hasn't reciprocated, the leaf gets downgraded to "claimed, unverified." Even if the parent's intent is to confirm, the protocol can't surface that intent before the file edit propagates. -- **No partner-side double-check primitive.** A "good partner" — say, a DSP about to extend governance trust through a leaf — has no way to ask the brand "is this really yours, with my use case in mind?" before acting. +- **No partner-side double-check primitive.** A "good partner" — a DSP about to extend brand-safety trust through a leaf, a creative-clearance pipeline confirming a trademark — has no way to ask the brand "is this really yours, with my use case in mind?" before acting. -The brand-agent surface (variant 3 / typed `agents[]`) already exists for this kind of authoritative-source pattern. `get_brand_identity` is the Tier 1 implementation. The verification tools are Tier 2. +**What this surface does fix.** Partners get typed, authoritative answers including the rich state crawling can't express. Stale crawler caches are no longer the trust floor. -This is **DRM-shaped for brand identity**: the brand owns its own answers to "what's mine," and authorized partners can ask before they act. It's not gating access to public data — the data is still public — it's providing nuanced, authoritative answers that crawling cannot. +**What it doesn't fix.** The sub-brand self-publishing problem (Converse team waiting for Nike's portfolio team to add the reciprocal entry, from PR #4505) is **relocated** by this surface, not solved. The partner gets `pending_review` instead of silence — a real UX improvement — but Converse still needs Nike's portfolio team to act. The agent-side workflow is the brand's responsibility; the protocol surfaces the state, it can't accelerate the parent's decision. ## Proposal @@ -40,21 +42,22 @@ This is **DRM-shaped for brand identity**: the brand owns its own answers to "wh Each tool follows the same shape: - **Input:** the claim being verified (subsidiary domain, property, trademark) -- **Output:** an enum status from a shared `VerificationStatus` vocabulary, plus tool-specific context +- **Output:** an enum status from the shared `VerificationStatus` vocabulary, plus tool-specific context The status enum captures the rich state the crawl model can't express: | Status | Meaning | |---|---| | `owned` | Definitively belongs to this brand, currently. | -| `pending_review` | The claim is known to the brand; no decision yet. Common for newly self-publishing sub-brand teams whose parent hasn't reciprocated. | +| `pending_review` | Claim known to the brand; no decision yet. **MUST be returned only with `expected_resolution_window_days`**, and the agent MUST transition to a terminal status or `unknown` once the window elapses. Without aging, `pending_review` becomes a polite shrug. | +| `transferring` | Ownership is provably changing — M&A in flight, divestiture closing, known imminent transition. Distinct from `pending_review` (under-review-by-us); `transferring` signals "the answer is known to be becoming something else." | | `disputed` | The brand actively rejects this claim. | -| `not_ours` | The brand affirms it is not their property. | -| `licensed_in` | This brand uses the asset under license from another entity (typed: `licensor_domain`). | +| `not_ours` | The brand affirms it is not their property / subsidiary / mark. | +| `licensed_in` | This brand uses the asset under license (response carries `licensor_domain`). | | `licensed_out` | This brand licenses the asset to another entity. | | `unknown` | The agent has no position. Caller MAY fall back to crawl. | -Not every tool returns every status — e.g., `verify_subsidiary_claim` doesn't return `licensed_in` / `licensed_out` (subsidiaries aren't licensed; brands and trademarks can be). The shared enum keeps callers' decoding logic uniform. +Each tool's response schema **constrains** the enum to the statuses applicable to that tool — not every status applies to every tool. Subsidiaries aren't licensed; properties don't have `pending_review` (use `transferring` for in-flight ownership); trademarks are mostly definitive but can be `transferring` during M&A. ### `verify_subsidiary_claim` @@ -69,19 +72,34 @@ A consumer detects a leaf-only edge: `converse.com` claims `house_domain: "nikei ``` ```json -// Response +// Response (mutual, signed by Nike's agent) +{ + "status": "owned", + "brand_id": "converse" +} +``` + +```json +// Response (pending — MUST include expected_resolution_window_days) { "status": "pending_review", + "brand_id": "converse", "first_observed_by_house_at": "2026-05-12T14:00:00Z", "expected_resolution_window_days": 7 } ``` -The agent's response is authoritative. The crawl backstop becomes unnecessary for the partner who has this answer. +The agent's signed response is authoritative. The crawl backstop becomes unnecessary for the partner who has this answer. ### `verify_property` -A DSP shopping inventory finds a property declaration on a brand's `properties[]` and wants to confirm before bidding. Or a fraud detector found a domain claiming to be Nike's and wants the brand's say-so: +This is **not a bid-time tool.** Sub-100ms auction budgets don't permit MCP round-trips. `verify_property` slots in at: + +- **Supply-path curation** — when an SSP onboards a new property as inventory and the buyer wants the brand's say-so before allowing bidding on it. +- **Fraud detection** — when a domain is suspected of falsely claiming brand affiliation. +- **Creative clearance** — when a creative-approval workflow encounters a property reference. + +Cache the answer (24-72h is appropriate for `owned`/`not_ours`); re-check periodically. ```json // Request @@ -94,20 +112,21 @@ A DSP shopping inventory finds a property declaration on a brand's `properties[] ``` ```json -// Response +// Response (owned) { "status": "owned", "relationship": "owned", + "brand_id": "nike", "regions": ["CN"], - "context": "Regional site for China market" + "context_note": "Regional site for China market" } ``` -The `relationship` field mirrors `brand.json`'s [property relationship enum](/docs/brand-protocol/brand-json#property-relationships) (`owned`, `direct`, `delegated`, `ad_network`). A `not_ours` response carries no `relationship`. +The `relationship` field mirrors `brand.json`'s [property relationship enum](/docs/brand-protocol/brand-json#property-relationships) (`owned`, `direct`, `delegated`, `ad_network`). ### `verify_trademark` -A creative-approval workflow encounters a mark and wants to verify ownership and use: +DoubleVerify, IAS, and creative-clearance pipelines already check trademark posture from registry crawls. **The differentiator here is the brand's authoritative statement about its licensee posture and authorized use cases** — registries can't tell you which use cases a brand authorizes for a mark, who the licensor is on a licensed-in mark, or whether the mark is mid-transfer. That's the gap this tool closes. ```json // Request @@ -119,12 +138,22 @@ A creative-approval workflow encounters a mark and wants to verify ownership and ``` ```json -// Response +// Response (owned, authorized caller with use cases) { "status": "owned", - "license_type": "owned", + "matched_registration": { + "registry": "USPTO", + "number": "1234567", + "mark": "AIR JORDAN", + "registration_status": "active" + }, "countries": ["US", "EU", "JP"], - "nice_classes": [25, 41] + "nice_classes": [25, 41], + "use_case_authorization": { + "advertising": true, + "endorsement": true, + "merchandise_resale": false + } } ``` @@ -146,65 +175,147 @@ Existing AdCP convention: a brand-agent advertises its tasks via `get_adcp_capab } ``` -Brand-agent implementations are free to support all, some, or none. A brand without a brand-agent (variants 1, 2, 4, 5 with no `agents[]` entry) falls back to the existing crawl-based mutual-assertion model and the `contact.email` SHOULD from PR #4505. +Brand-agent implementations are free to support all, some, or none. A brand without a brand-agent (variants 1, 2, 4, 5 with no `agents[]` entry, or an agent that doesn't advertise these tasks) falls back to the existing crawl-based mutual-assertion model and the `contact.email` SHOULD from PR #4505. When a brand-agent is advertised and the relevant tool is in `supported_tasks`, the [Conformance](/docs/brand-protocol/brand-json#conformance) clause around mutual-assertion verification SHOULD prefer the agent's authoritative answer over the crawl-based inference. The crawl path remains the default; the agent path is a discoverable upgrade. -## Authorization and access tiers +## Authorization tiers -Verification tools follow the same public/authorized split as `get_brand_identity`: +Verification tools follow the same public/authorized split as `get_brand_identity`. **The tier table here is the authoritative reference; task pages refer back to it.** | Tier | What the agent returns | |---|---| -| **Public** (no linked account) | Status enum + minimal context. "Is this mine?" gets a yes/no/pending answer. | -| **Authorized** (linked via [`sync_accounts`](/docs/accounts/tasks/sync_accounts)) | Status + richer context (registration details, regions, use case eligibility, expected resolution window for pending). | +| **Public** (no linked account) | `status` (always). For owned/transferring cases: `brand_id`, `relationship` (property), `matched_registration` + `countries` + `nice_classes` (trademark). For `disputed`/`not_ours`/`pending_review`: `context_note`. | +| **Authorized** (linked via [`sync_accounts`](/docs/accounts/tasks/sync_accounts)) | Everything above, plus: `first_observed_by_house_at`, `expected_resolution_window_days`, `use_case_authorization`. | -A consumer that is NOT authorized still gets a useful answer — the binary "is this yours" — without exposing the brand's internal queue state or licensing detail. Authorized consumers (e.g., a DSP that has linked accounts with the brand for inventory shopping) get the nuanced state. +Authorized tier is **a wedge for `sync_accounts` adoption**. The `use_case_authorization` map — does the brand authorize this use case for this asset? — is the highest-value field for a DSP that's already shopping inventory with the brand. It's not derivable from any registry crawl. Account-linking gets the partner the answer registries can't give. -The `status` enum is always returned at both tiers. Optional fields (`first_observed_by_house_at`, `licensor_domain`, `expected_resolution_window_days`) are gated. +Queue position, internal ticket state, and team routing are NEVER exposed — not to the public, not to authorized callers. ## Trust model -The agent's response is **authoritative**: signed under the brand's `adcp_use: "request-signing"` JWK (existing AdCP convention) and consumed under that contract. A signed `not_ours` from the brand's own agent overrides a third-party leaf's `house_domain` claim — the brand has spoken and is the authority over its own ownership. +The agent's response is **authoritative**: signed under the brand's `adcp_use: "response-signing"` JWK (matching the AdCP webhook-signing convention — distinct from `request-signing` per the [keys-per-purpose](https://datatracker.ietf.org/doc/html/rfc9421) requirement). Consumers verify the signature against the brand's published JWKS before extending trust. + +**Normative conflict resolution.** When agent and crawl disagree: + +| Agent says | Crawl observes | Consumer treats as | +|---|---|---| +| `owned` | Mutual or leaf-only | Trusted edge. Governance propagation: yes. | +| `pending_review` | Leaf-only (parent silent) | Trusted leaf identity; relationship trust withheld until window elapses or status transitions. | +| `transferring` | Any | Trusted but in-flight; consumers MAY treat as `owned` for stability and SHOULD surface the transition in UI. | +| `disputed` | Leaf-only or mutual | The brand has spoken. **Leaf is treated as having a rejected parent claim.** UI: "Brand X says this is not theirs." | +| `not_ours` | Leaf-only or mutual | The brand affirms no relationship. **Leaf treated as standalone with a disputed parent claim, regardless of leaf's `house_domain` declaration.** UI surfaces the rejection. | +| `unknown` | Any | Fall back to crawl-based mutual-assertion inference. | + +**When the agent answers, the agent wins.** This is normative, not advisory. The crawl path remains the fallback only when the agent is unreachable, returns `unknown`, or doesn't implement the task. + +A leaf publisher whose `house_domain` claim is rejected by the named house's agent (`disputed` or `not_ours`) has **no protocol-level recourse** to override the rejection. The leaf MAY update its `house_domain` (or remove it for standalone status) and re-publish; the brand-side response will update on the agent's next refresh. UI guidance for rendering this state on consumer surfaces is a [follow-up](https://github.com/adcontextprotocol/adcp/issues) (tracked separately). + +## Caching (normative) + +Verification responses SHOULD be cacheable per standard HTTP semantics. Agents SHOULD set `Cache-Control: max-age=N` appropriate to the volatility of the answer: + +- `owned` / `not_ours` / `disputed` — stable. 24-72h is reasonable. +- `pending_review` — volatile. Re-check on each governance decision; max-age SHOULD be short (≤1h). +- `transferring` — volatile. Re-check until status transitions; max-age SHOULD be short (≤4h). +- `licensed_in` / `licensed_out` — moderately volatile. 24h is reasonable; re-check on contract-affected workflows. +- `use_case_authorization` — most volatile field. SHOULD be re-checked per session. +- `unknown` — short cache only (≤1h) so consumers can re-query as the agent's knowledge grows. + +Consumers MAY override agent-supplied cache hints downward. A consumer SHOULD NOT cache beyond the agent's supplied `max-age`. + +## Rate limiting (normative expectation) + +Agents MAY rate-limit per `{caller_identity, tool, query_target}`. Agents SHOULD: + +- Return `Retry-After` header on rate-limited responses so callers can back off cleanly. +- Prefer returning the cached prior answer (with a fresh `Cache-Control` allowing the caller to use it) over a `RATE_LIMITED` error — graceful degradation beats hard failure. + +Callers' `idempotency_key` (from `version-envelope`) lets retries deduplicate; agents SHOULD recognize repeated keys within their TTL window. + +## Deployment model + +Two practical deployment shapes: -The crawl-based mutual-assertion model remains the fallback when: -- No brand-agent is advertised in the brand.json `agents[]` -- The advertised agent doesn't implement the relevant task -- The agent's response is `unknown` -- The agent is unreachable (transport-level failure) +**Hosted by AAO (most members).** AdContextProtocol.org operates brand-agents as a managed service for members. AAO ingests member-supplied data + portfolio-team workflows and exposes the verification tools per the standardized contract. Members configure their answers; the protocol surface is uniform. -When both paths exist and the agent answers, the agent wins. +**Self-hosted.** Holdcos, agencies, and any party with engineering bandwidth implements the brand-agent themselves. They publish the agent URL in their brand.json `agents[]`, sign responses with their own `response-signing` JWK, and implement whatever queue/triage system fits their portfolio operations. -## Internal house responsibility — the self-healing loop +**No brand-agent.** Most brands today. Falls back to the crawl-based mutual-assertion model plus the email-based self-healing SHOULD from PR #4505. The protocol degrades cleanly. -The brand-agent is responsible for what was previously the email-notification surface. When a consumer asks `verify_subsidiary_claim` and the brand has never seen the claim before, the agent: +This RFC defines the contract; the AAO product team's implementation is tracked separately as a [follow-up](https://github.com/adcontextprotocol/adcp/issues). -1. Returns `pending_review` to the caller (or `unknown` if it can't classify). -2. **Internally** logs the inbound claim, optionally notifies the house's portfolio team, and queues the claim for review. +## End-to-end example: Nike, Inc. -This is implementation-defined and not part of the protocol contract. The protocol just provides the entry point; what the brand does internally is its business. The [PR #4505 self-healing SHOULD](/docs/brand-protocol/brand-json#self-healing-through-notification) effectively migrates from "consumer emails the house" to "the agent is the house's intake." For houses without a brand-agent, the email SHOULD remains. +A complete walkthrough for a partner verifying the full Nike portfolio. Assumes Nike publishes a brand-agent at `https://agent.nikeinc.com/mcp` advertising all three verification tools. + +**1. Discovery.** Partner reads `nikeinc.com/.well-known/brand.json`, finds the `agents[]` entry: + +```json +{ "type": "brand", "url": "https://agent.nikeinc.com/mcp", "id": "nike_inc_brand_agent" } +``` + +**2. Capability check.** Partner calls `get_adcp_capabilities` on the agent. Response includes `verify_subsidiary_claim`, `verify_property`, `verify_trademark` in `supported_tasks`. + +**3. Subsidiary verification.** Partner discovered `converse.com` claiming `house_domain: nikeinc.com`. Calls `verify_subsidiary_claim`: + +```json +{ "claimed_subsidiary_domain": "converse.com", "claimed_brand_id": "converse" } +→ +{ "status": "owned", "brand_id": "converse" } +``` + +**4. Property verification.** A property `jordan.com` is in scope. Partner calls `verify_property`: + +```json +{ "property": { "type": "website", "identifier": "jordan.com" }, "use_case": "advertising" } +→ +{ "status": "owned", "relationship": "owned", "brand_id": "jordan", "use_case_authorization": { "advertising": true } } +``` + +**5. Trademark verification.** A creative includes "AIR JORDAN." Partner calls `verify_trademark`: + +```json +{ "mark": "AIR JORDAN", "registry": "USPTO" } +→ +{ "status": "owned", "matched_registration": {...}, "use_case_authorization": { "advertising": true, "merchandise_resale": false } } +``` + +**6. Decision.** Partner has authoritative confirmation across three dimensions. The crawl-based mutual-assertion path was never invoked. Cache for 24h per agent's `Cache-Control` headers; re-validate on next campaign cycle. ## Out of scope Three categories the verification surface deliberately doesn't cover: - **Creative conflict / brand-safety scoring.** "Is this creative in conflict with our guidelines?" is its own design space — overlaps with creative protocol and brand-safety vendors. May land as a separate RFC. -- **Competitive relationships.** "Is this brand competitive to us?" is interesting but politically loaded; a brand declaring competitors machine-readably has different incentives than declaring its own assets. Deferred. -- **Cryptographic provenance** of agent-issued statements. Today: rely on transport-level signing (RFC 9421) and `adcp_use`-tagged keys. Cryptographic claim-chains (e.g., "this trademark assertion is signed by the registrar plus the brand") are out of scope. +- **Competitive relationships.** "Is this brand competitive to us?" is **indefinitely deferred.** A brand declaring competitors machine-readably has different incentives than declaring its own assets (legal exposure, antitrust optics). The protocol won't ship a primitive that asks brands to publicly enumerate enemies. +- **Cryptographic provenance** of agent-issued statements beyond transport-level signing. Today: rely on RFC 9421 and `adcp_use`-tagged keys. Cryptographic claim-chains (e.g., "this trademark assertion is signed by the registrar plus the brand") are out of scope. + +## Prior art and adjacent standards + +No direct equivalent to brand-side authoritative verification exists in IAB Tech Lab or TAG today. The closest analogs: + +- **ads.txt / sellers.json / app-ads.txt** — authority over *inventory*. This RFC's surface is authority over *identity*. Complementary, not duplicative. +- **WHOIS / RDAP** — authority over *domain registration*. Different layer; doesn't carry brand-licensing or use-case authorization. +- **Trademark registry crawls** (USPTO, EUIPO, etc.) — authority over *registration facts*. The differentiator here is licensee-side posture and authorized use cases, which registries can't speak to. + +Within AdCP, the [provenance verifier contract](https://github.com/adcontextprotocol/adcp/pull/3468) (seller-publishes / buyer-represents / seller-confirms) uses an adjacent construction for a different field family. The verification surface is closer to a query API than a publish-verify pair, but the trust shape (brand publishes its agent, partners verify against the brand's published JWKS) is the same. ## Migration No migration burden. All three tools are additive — a brand-agent that doesn't implement them simply doesn't advertise them. Consumers that don't speak these tools fall back to the existing crawl + `contact.email` paths. -The [Conformance](/docs/brand-protocol/brand-json#conformance) update is one sentence: "Where the named house publishes a brand-agent advertising `verify_subsidiary_claim` in `supported_tasks`, consumers SHOULD prefer the agent's response over crawl-based mutual-assertion inference." +The [Conformance](/docs/brand-protocol/brand-json#conformance) update is one SHOULD: "Where the named house publishes a brand-agent advertising `verify_subsidiary_claim` in `supported_tasks`, consumers SHOULD prefer the agent's response over crawl-based mutual-assertion inference." + +## Resolved decisions (formerly open questions) -## Open questions +The following were open during draft and are now resolved as the spec contract: -1. **Should `verify_property` accept a `use_case` field** (advertising / endorsement / retail / etc.) that the agent can scope its answer to? E.g., a brand might own a domain but not authorize ad-network resale on it. Vote: yes, mirror the `get_brand_identity` `use_case` pattern. -2. **Should `pending_review` include the queue position** or expected resolution time as authorized-tier data, or is `expected_resolution_window_days` (an aggregate) enough? Vote: aggregate only — exposing queue position leaks brand-internal operations. -3. **What happens when a brand-agent answers `not_ours` and the leaf publishes `house_domain` pointing at the brand?** Spec says agent wins (treat leaf as having a disputed parent claim). UI guidance: surface "disputed" prominently — this is the brand telling the consumer "we reject this." Vote: confirm. -4. **Rate-limiting.** The agent decides, but the spec should set expectations. Vote: agent MAY rate-limit per `{caller_identity, tool, query_target}` pair; caller's `idempotency_key` (from `version-envelope`) lets it retry without duplicate side effects. -5. **Bulk variants** (`verify_subsidiary_claims`, plural). For a crawler verifying 100 leaves at once, a bulk API reduces round-trips. Vote: defer — single-target tools cover v1; bulk is additive when call volume justifies it. +1. **`verify_property.use_case` field.** Kept. Mirrors the `get_brand_identity` `use_case` precedent. +2. **`pending_review` queue position vs aggregate window.** Aggregate only (`expected_resolution_window_days`). Queue position leaks operational state. Additionally: `pending_review` MUST come with the window AND the agent MUST transition or flip to `unknown` past the window — an aging contract, not a polite shrug. +3. **Agent says `not_ours` while leaf publishes `house_domain` claim.** Normative: agent wins. See [Trust model § conflict resolution](#trust-model). UI guidance for leaf's recourse is a follow-up issue. +4. **Rate-limiting.** Agent's call; spec sets the expectation of per-`{caller, tool, target}`. Agents SHOULD return `Retry-After` on limit and SHOULD prefer cached-prior-answer over `RATE_LIMITED` error. +5. **Bulk variants** (`verify_subsidiary_claims`, plural). Deferred. Filed as follow-up — crawlers will demand it within 6 months; v1 ships single-target shape. ## References @@ -213,3 +324,6 @@ The [Conformance](/docs/brand-protocol/brand-json#conformance) update is one sen - [`brand.json`](/docs/brand-protocol/brand-json) — normative spec for the underlying mutual-assertion trust model - [`building-a-brand-agent`](/docs/brand-protocol/building-a-brand-agent) — Tier 1 (`get_brand_identity`) reference; Tier 2 (this RFC) implementation guide is a planned addition - [`get_brand_identity`](/docs/brand-protocol/tasks/get_brand_identity) — the existing tool whose pattern these verifications follow +- [`verify_subsidiary_claim`](/docs/brand-protocol/tasks/verify_subsidiary_claim) — task reference +- [`verify_property`](/docs/brand-protocol/tasks/verify_property) — task reference +- [`verify_trademark`](/docs/brand-protocol/tasks/verify_trademark) — task reference diff --git a/docs/brand-protocol/tasks/verify_property.mdx b/docs/brand-protocol/tasks/verify_property.mdx index 2cfc8bbaa1..7c26284cc7 100644 --- a/docs/brand-protocol/tasks/verify_property.mdx +++ b/docs/brand-protocol/tasks/verify_property.mdx @@ -9,25 +9,40 @@ testable: true **Proposed (RFC).** This task is part of the [brand verification RFC](/docs/brand-protocol/proposals/brand-verification-rfc) and not yet normative. -Ask a brand-agent whether a property (website, app, podcast, etc.) belongs to this brand. Used by DSPs verifying inventory ownership, fraud detectors confirming domain claims, or any consumer that wants the brand's authoritative answer rather than inferring from a crawl of `properties[]`. +Ask a brand-agent whether a property (website, app, podcast, etc.) belongs to this brand. Used in **inventory onboarding / supply-path-curation workflows**, fraud detection, and creative-clearance — not at bid time (sub-100ms auction budgets don't accommodate MCP round-trips). Cache and re-validate periodically. ## Schema - **Request**: [`verify-property-request.json`](https://adcontextprotocol.org/schemas/v3/brand/verify-property-request.json) - **Response**: [`verify-property-response.json`](https://adcontextprotocol.org/schemas/v3/brand/verify-property-response.json) +## Capability discovery + +The brand-agent advertises this task in its `get_adcp_capabilities` response (alongside any other [verification tools](/docs/brand-protocol/proposals/brand-verification-rfc) it implements): + +```json +{ + "supported_protocols": ["brand"], + "supported_tasks": ["get_brand_identity", "verify_property"] +} +``` + ## When to use -A DSP about to bid on inventory associated with `nike.cn` wants to confirm Nike owns it before extending brand-safety trust. A fraud detector found a domain claiming to be Nike's and wants the brand's say-so. A creative-approval workflow encountered a property reference and needs ownership confirmation. +- **Supply-path curation.** An SSP onboards a property as inventory and the buyer wants the brand's say-so before allowing bidding on it. +- **Fraud detection.** A domain is suspected of falsely claiming brand affiliation. +- **Creative clearance.** A creative-approval workflow encounters a property reference and needs ownership confirmation. -The static `brand.json` `properties[]` array is the published self-declaration. `verify_property` is the **real-time authoritative** answer from the brand-agent, including the `relationship` enum (owned / direct / delegated / ad_network) that mirrors the static declaration plus optional `use_case_authorization` gating. +Not bid-time. Cache 24-72h for `owned`/`not_ours`; less for `transferring`/`pending` states. -## Public by default +## Authorization tiers | Tier | What you get | |---|---| -| **Public** | `status`, `relationship` (when owned), `brand_id`, `regions`, `context_note`. | -| **Authorized** | Everything above, plus `use_case_authorization` — per-use-case permission flags. | +| **Public** | `status`, `relationship` (when owned/transferring), `brand_id`, `regions`, `context_note`. | +| **Authorized** (via [`sync_accounts`](/docs/accounts/tasks/sync_accounts)) | Everything above, plus `use_case_authorization` — per-use-case permission flags. | + +The full tier table is in [the RFC](/docs/brand-protocol/proposals/brand-verification-rfc#authorization-tiers). ## Quick start @@ -52,17 +67,18 @@ The static `brand.json` `properties[]` array is the published self-declaration. } ``` -```json Request (verify an app bundle) +```json Request (verify an app bundle with use case) { "property": { "type": "mobile_app", "identifier": "com.nike.snkrs", "store": "apple" - } + }, + "use_case": "advertising" } ``` -```json Response (owned, authorized caller with use case) +```json Response (owned, authorized caller) { "status": "owned", "relationship": "owned", @@ -74,6 +90,15 @@ The static `brand.json` `properties[]` array is the published self-declaration. } ``` +```json Response (transferring) +{ + "status": "transferring", + "relationship": "owned", + "brand_id": "nike", + "context_note": "Domain transferring to subsidiary on 2026-09-01; treat as owned." +} +``` + ```json Response (disputed) { "status": "disputed", @@ -86,31 +111,55 @@ The static `brand.json` `properties[]` array is the published self-declaration. "status": "not_ours" } ``` + +```json Response (error) +{ + "errors": [ + { + "code": "INVALID_INPUT", + "message": "property.identifier must be a valid hostname or app bundle id." + } + ] +} +``` ## Relationship and use case -The `relationship` field — present when status is `owned` — answers "the brand controls this property, but commercially how?": +The `relationship` field — present when status is `owned` or `transferring` — answers "the brand controls this property, but commercially how?": - `owned` — brand owns and operates the property directly. - `direct` — brand is the direct sales path even if a third party runs the tech. - `delegated` — brand has delegated monetization (e.g., Mediavine for a food blog). - `ad_network` — brand sells as part of a network/exchange. -The `use_case` request field — and the corresponding `use_case_authorization` response — handle the orthogonal question of "is the named use case allowed on this property?" A brand may own a domain but not authorize ad-network resale on it; the use-case gate exposes that without leaking the brand's full commercial policy. +The `use_case` request field — and the `use_case_authorization` response — handle the orthogonal question of "is the named use case allowed?" A brand may own a domain but not authorize ad-network resale on it. The registered use-case keys are `advertising`, `endorsement`, `retail_listing`, `editorial`, `commercial_advertising`, `merchandise_resale`. Agents MAY add additional keys. ## Trust model -Identical to [`verify_subsidiary_claim`](/docs/brand-protocol/tasks/verify_subsidiary_claim#trust-model). Signed authoritative response from the agent overrides static `brand.json` `properties[]` inference. When the agent is unreachable or returns `unknown`, fall back to crawl-based inference from the static file. +The agent's response is **authoritative** when signed under the brand's `adcp_use: "response-signing"` JWK. Consumers SHOULD prefer the agent's response over `brand.json` `properties[]` inference. Conflict resolution mirrors [`verify_subsidiary_claim` § Trust model](/docs/brand-protocol/tasks/verify_subsidiary_claim#trust-model): -## Caching +| Agent says | Consumer treats as | +|---|---| +| `owned` | Trusted; bid/curate against `properties[]` confirmation. | +| `transferring` | Trusted; MAY treat as owned for stability; surface in UI. | +| `disputed` | Brand rejects this property claim. Do not extend trust. | +| `not_ours` | Brand affirms no relationship. | +| `unknown` | Fall back to `brand.json` `properties[]` inference. | -Properties churn less than subsidiary claims. Cache-friendly: +When the agent answers, the agent wins. -- `owned` / `not_ours` — stable. 24-72h cache. -- `disputed` — stable until disputed party changes claim. 24h cache. -- `use_case_authorization` — MAY be more volatile (a brand might rotate ad-network authorization). Re-check per session is reasonable. +## Caching + +- `owned` / `not_ours` / `disputed` — stable. 24-72h cache. +- `transferring` — volatile until transition. Max-age ≤4h. +- `use_case_authorization` — most volatile. Re-check per session. +- `unknown` — short cache (≤1h). ## Error handling -See [`verify_subsidiary_claim` § Error handling](/docs/brand-protocol/tasks/verify_subsidiary_claim#error-handling). Same error code surface applies. +| Error code | Cause | +|---|---| +| `AUTH_INVALID` | Caller's signed envelope did not verify. | +| `RATE_LIMITED` | Agent has rate-limited the caller. Agents SHOULD return `Retry-After` and prefer cached prior answer over hard error. | +| `INVALID_INPUT` | `property.identifier` is not valid for the declared `type`. | diff --git a/docs/brand-protocol/tasks/verify_subsidiary_claim.mdx b/docs/brand-protocol/tasks/verify_subsidiary_claim.mdx index c5576c8a90..725190f4c6 100644 --- a/docs/brand-protocol/tasks/verify_subsidiary_claim.mdx +++ b/docs/brand-protocol/tasks/verify_subsidiary_claim.mdx @@ -1,6 +1,6 @@ --- title: verify_subsidiary_claim -description: "verify_subsidiary_claim is the AdCP brand-protocol task for asking a brand-agent whether a named brand is a subsidiary of this house. Returns an authoritative status — owned, pending_review, disputed, not_ours, or unknown — used in place of crawl-based mutual-assertion inference when the brand-agent supports it." +description: "verify_subsidiary_claim is the AdCP brand-protocol task for asking a brand-agent whether a named brand is a subsidiary of this house. Returns an authoritative status — owned, pending_review, transferring, disputed, not_ours, or unknown — used in place of crawl-based mutual-assertion inference when the brand-agent supports it." "og:title": "AdCP — verify_subsidiary_claim" testable: true --- @@ -16,26 +16,42 @@ Ask a brand-agent whether the named brand is a subsidiary of this house. Returns - **Request**: [`verify-subsidiary-claim-request.json`](https://adcontextprotocol.org/schemas/v3/brand/verify-subsidiary-claim-request.json) - **Response**: [`verify-subsidiary-claim-response.json`](https://adcontextprotocol.org/schemas/v3/brand/verify-subsidiary-claim-response.json) +## Capability discovery + +The brand-agent advertises this task in its `get_adcp_capabilities` response: + +```json +{ + "supported_protocols": ["brand"], + "supported_tasks": [ + "get_brand_identity", + "verify_subsidiary_claim" + ] +} +``` + +Callers MUST check capability advertisement before relying on this task; brand-agents may implement any subset of the [verification surface](/docs/brand-protocol/proposals/brand-verification-rfc). + ## When to use The crawl-based mutual-assertion model — leaf publishes `house_domain: X`, X's `brand_refs[]` includes leaf, both halves verified by independent fetch — covers the common case. But it has known gaps: -- **Two-state visibility.** Crawl answers "mutual" or "not mutual." It can't surface *pending review* or *disputed*. +- **Two-state visibility.** Crawl answers "mutual" or "not mutual." It can't surface *pending review*, *transferring*, or *disputed*. - **TTL-bound freshness.** A consumer's view is only as fresh as its last crawl of both sides. - **One-sided pessimism.** A leaf-only edge gets downgraded to "claimed, unverified" even when the brand intends to confirm. -`verify_subsidiary_claim` asks the brand directly. A signed `owned` from the brand's own agent is stronger than a published `brand_refs[]` entry (since it's real-time). A signed `not_ours` overrides a leaf's `house_domain` claim. And `pending_review` surfaces a state the crawl model simply can't. +`verify_subsidiary_claim` asks the brand directly. A signed `owned` from the brand's own agent is real-time-authoritative — stronger than a published `brand_refs[]` entry. A signed `not_ours` overrides a leaf's `house_domain` claim. And `pending_review` / `transferring` surface states the crawl model simply can't. -## Public by default +## Authorization tiers -Every brand-agent that implements this task MUST return a `status` to any caller. Authorized callers (linked via [`sync_accounts`](/docs/accounts/tasks/sync_accounts)) additionally receive richer context — `first_observed_by_house_at`, `expected_resolution_window_days`, and the brand's `brand_id` for the subsidiary. +Verification tools follow the same public/authorized split as `get_brand_identity`. The authoritative tier table lives in [the RFC](/docs/brand-protocol/proposals/brand-verification-rfc#authorization-tiers); the short version for this tool: | Tier | What you get | |---|---| -| **Public** | `status`, public `brand_id` (when applicable), public `dispute_reason` (when status is `disputed`). | -| **Authorized** | Everything above, plus `first_observed_by_house_at` and `expected_resolution_window_days`. | +| **Public** | `status`, public `brand_id` (when applicable), `context_note` (when populated by the brand — carries dispute rationale, transfer details, etc). | +| **Authorized** (via [`sync_accounts`](/docs/accounts/tasks/sync_accounts)) | Everything above, plus `first_observed_by_house_at` and `expected_resolution_window_days`. | -Queue position, internal ticket state, and team routing are NEVER exposed — neither to the public nor to authorized callers. +Queue position, internal ticket state, and team routing are never exposed. ## Quick start @@ -48,14 +64,14 @@ Queue position, internal ticket state, and team routing are NEVER exposed — ne } ``` -```json Response (mutual) +```json Response (owned) { "status": "owned", "brand_id": "converse" } ``` -```json Response (pending, authorized caller) +```json Response (pending, authorized) { "status": "pending_review", "brand_id": "converse", @@ -64,10 +80,18 @@ Queue position, internal ticket state, and team routing are NEVER exposed — ne } ``` +```json Response (transferring) +{ + "status": "transferring", + "brand_id": "converse", + "context_note": "Pending divestiture; closes 2026-08-15. Treat as owned for stability." +} +``` + ```json Response (disputed) { "status": "disputed", - "dispute_reason": "We do not own this brand; trademark held by separate entity." + "context_note": "We do not own this brand; trademark held by separate entity." } ``` @@ -82,40 +106,55 @@ Queue position, internal ticket state, and team routing are NEVER exposed — ne "status": "unknown" } ``` + +```json Response (error) +{ + "errors": [ + { + "code": "AUTH_INVALID", + "message": "Signed envelope did not verify against any published JWK." + } + ] +} +``` ## Trust model -The agent's response is **authoritative** when signed under the brand's `adcp_use: "request-signing"` JWK. Consumers SHOULD prefer the agent's response over crawl-based mutual-assertion inference when the brand-agent advertises `verify_subsidiary_claim` in its `supported_tasks`. +The agent's response is **authoritative** when signed under the brand's `adcp_use: "response-signing"` JWK (the AdCP webhook-signing convention applies to response-side signing too; the key purpose is distinct from `request-signing`). Consumers SHOULD prefer the agent's response over crawl-based mutual-assertion inference when the brand-agent advertises `verify_subsidiary_claim` in its `supported_tasks`. -Conflict resolution between agent response and crawl observation: +Conflict resolution between agent response and crawl observation (normative — see [RFC § Trust model](/docs/brand-protocol/proposals/brand-verification-rfc#trust-model)): | Agent says | Crawl says | Consumer treats as | |---|---|---| | `owned` | Mutual or leaf-only | Trusted edge. Governance propagation: yes. | -| `pending_review` | Leaf-only (parent silent) | Trusted leaf identity; relationship trust withheld. UI: "Pending parent confirmation, expected within N days." | +| `pending_review` | Leaf-only (parent silent) | Trusted leaf identity; relationship trust withheld until window elapses or status transitions. UI: "Pending parent confirmation, expected within N days." | +| `transferring` | Any | Trusted but in-flight; consumers MAY treat as `owned` for stability and SHOULD surface the transition in UI. | | `disputed` | Leaf-only or mutual | The brand has spoken. Treat leaf as having a rejected parent claim. UI: "Brand X says this is not theirs." | -| `not_ours` | Leaf-only | Leaf is misclassified; treat as standalone with disputed parent claim. | +| `not_ours` | Leaf-only | Leaf treated as standalone with disputed parent claim. | | `unknown` | Any | Fall back to crawl-based mutual-assertion inference. | -When the agent answers, the agent wins. The crawl path remains as fallback only when the agent is unreachable, `unknown`, or doesn't implement the task. +When the agent answers, the agent wins. The crawl path remains the fallback only when the agent is unreachable, returns `unknown`, or doesn't implement the task. + +`pending_review` requires the agent to set `expected_resolution_window_days` AND transition the claim to a terminal status (or `unknown`) once the window elapses. Staleness past the window is a contract violation; consumers MAY treat such responses as `unknown`. ## Caching -Brand-agent responses to this task SHOULD carry standard HTTP cache headers and a TTL appropriate to the volatility: +Verification responses SHOULD be cacheable per standard HTTP semantics. Agents SHOULD set `Cache-Control: max-age=N`: -- `owned` / `not_ours` — stable. 24h cache is reasonable. -- `pending_review` — volatile. Re-check on each governance decision. -- `disputed` — stable until disputed party publishes new claim. 24h cache is reasonable. +- `owned` / `not_ours` / `disputed` — stable. 24-72h is reasonable. +- `pending_review` — volatile. Re-check per governance decision; max-age ≤1h. +- `transferring` — volatile until transition. Max-age ≤4h. +- `unknown` — short cache (≤1h). -The agent MAY return `Cache-Control: max-age=N` to constrain consumer caching. +Consumers MAY override agent-supplied cache hints downward but SHOULD NOT cache beyond the agent's `max-age`. ## Error handling -Tasks return the `VerifySubsidiaryClaimError` arm on transport/auth failures: +The `VerifySubsidiaryClaimError` arm returns on transport/auth/input failures. Operational failures (agent's internal database is down, etc.) MAY return `status: "unknown"` instead of an error, so the caller can fall back to crawl. -- `AUTH_INVALID` — caller's signed envelope did not verify. (Distinct from `AUTH_MISSING`: an unauthenticated public call returns the public success arm with `status`, not an error.) -- `RATE_LIMITED` — agent has rate-limited the caller (typically per `{caller_identity, claimed_subsidiary_domain}`). -- `INVALID_INPUT` — `claimed_subsidiary_domain` is not a valid hostname or fails the agent's input policy. - -Operational failures (the agent's internal database is down, etc.) MAY return `status: "unknown"` rather than an error — the caller can fall back to crawl. +| Error code | Cause | +|---|---| +| `AUTH_INVALID` | Caller's signed envelope did not verify. (Distinct from `AUTH_MISSING`: an unauthenticated public call returns the public success arm with `status`, not an error.) | +| `RATE_LIMITED` | Agent has rate-limited the caller. Agents SHOULD return `Retry-After` and SHOULD prefer returning a cached prior answer over a hard error. | +| `INVALID_INPUT` | `claimed_subsidiary_domain` is not a valid hostname or fails the agent's input policy. | diff --git a/docs/brand-protocol/tasks/verify_trademark.mdx b/docs/brand-protocol/tasks/verify_trademark.mdx index 26b42c4b84..1c674792d8 100644 --- a/docs/brand-protocol/tasks/verify_trademark.mdx +++ b/docs/brand-protocol/tasks/verify_trademark.mdx @@ -1,6 +1,6 @@ --- title: verify_trademark -description: "verify_trademark is the AdCP brand-protocol task for asking a brand-agent whether a trademark is owned, licensed in, or licensed out by this brand. Returns registration details, jurisdictions, Nice classes, and optional per-use-case authorization." +description: "verify_trademark is the AdCP brand-protocol task for asking a brand-agent whether a trademark is owned, licensed in, or licensed out by this brand. The differentiator vs registry crawls: authoritative licensee posture and per-use-case authorization that registries cannot provide." "og:title": "AdCP — verify_trademark" testable: true --- @@ -9,31 +9,43 @@ testable: true **Proposed (RFC).** This task is part of the [brand verification RFC](/docs/brand-protocol/proposals/brand-verification-rfc) and not yet normative. -Ask a brand-agent whether a trademark is owned, licensed in, or licensed out by this brand. Used in creative approval workflows, brand-safety pipelines, and any consumer that needs the brand's authoritative ownership statement over a mark. +Ask a brand-agent whether a trademark is owned, licensed in, or licensed out by this brand. The differentiator from registry crawls: registries can tell you a mark exists, who registered it, and the registration status — they can't tell you which use cases the brand authorizes, who the licensor is on a licensed-in mark, or whether the mark is mid-transfer. This tool fills that gap. ## Schema - **Request**: [`verify-trademark-request.json`](https://adcontextprotocol.org/schemas/v3/brand/verify-trademark-request.json) - **Response**: [`verify-trademark-response.json`](https://adcontextprotocol.org/schemas/v3/brand/verify-trademark-response.json) +## Capability discovery + +The brand-agent advertises this task in its `get_adcp_capabilities` response: + +```json +{ + "supported_protocols": ["brand"], + "supported_tasks": ["get_brand_identity", "verify_trademark"] +} +``` + ## When to use -Trademark ownership has gnarly edges that static publication can't always resolve: +Trademark ownership has gnarly edges that static publication and registry crawls can't always resolve: - **Cross-jurisdiction conflicts.** USPTO `CONVERSE` and EUIPO `CONVERSE` may be held by different parties. - **Nice class disambiguation.** `DELTA` (airline, class 39) and `DELTA` (faucet, class 11) are different brands with the same mark. -- **Licensing chains.** A brand uses a mark under license (`licensed_in`) or licenses it out (`licensed_out`) — neither is captured cleanly by static publication alone. +- **Licensing chains.** A brand uses a mark under license (`licensed_in`) or licenses it out (`licensed_out`) — neither is captured cleanly by static publication or registries. +- **Authorized use cases.** Whether the brand permits a mark for advertising vs editorial vs merchandise is brand-policy, not registry-fact. -`verify_trademark` lets a consumer ask the brand's authoritative agent for a definitive answer, including the matched registration and the licensing relationship. +`verify_trademark` lets a consumer ask the brand's authoritative agent for a definitive answer, including the matched registration, licensing relationship, and per-use-case authorization. -## Public by default +## Authorization tiers | Tier | What you get | |---|---| -| **Public** | `status`, `matched_registration`, `license_type`, `licensor_domain` (when `licensed_in`), `countries`, `nice_classes`, `context_note`. | -| **Authorized** | Everything above, plus `use_case_authorization` — per-use-case permission flags. | +| **Public** | `status`, `matched_registration`, `licensor_domain` (when `licensed_in`), `countries`, `nice_classes`, `context_note`. | +| **Authorized** (via [`sync_accounts`](/docs/accounts/tasks/sync_accounts)) | Everything above, plus `use_case_authorization` — the highest-value field, derivable from no registry crawl. | -Trademark facts are largely public-record (registry filings are open). The agent surfaces this for callers who don't want to crawl every registry; authorized callers additionally learn which use cases the brand authorizes for the mark. +Trademark facts are largely public-record. The authorized tier exposes brand-side policy that registries can't speak to. The full tier table is in [the RFC](/docs/brand-protocol/proposals/brand-verification-rfc#authorization-tiers). ## Quick start @@ -53,9 +65,8 @@ Trademark facts are largely public-record (registry filings are open). The agent "registry": "USPTO", "number": "1234567", "mark": "AIR JORDAN", - "status": "active" + "registration_status": "active" }, - "license_type": "owned", "countries": ["US"], "nice_classes": [25, 41] } @@ -64,7 +75,7 @@ Trademark facts are largely public-record (registry filings are open). The agent ```json Request (verify by mark + region) { "mark": "CONVERSE", - "countries": ["EU"] + "countries": ["FR", "DE"] } ``` @@ -75,9 +86,8 @@ Trademark facts are largely public-record (registry filings are open). The agent "registry": "EUIPO", "number": "EU98765", "mark": "CONVERSE", - "status": "active" + "registration_status": "active" }, - "license_type": "licensed_in", "licensor_domain": "converseholdings-eu.com", "countries": ["FR", "DE", "IT", "ES"], "nice_classes": [25] @@ -87,8 +97,7 @@ Trademark facts are largely public-record (registry filings are open). The agent ```json Response (authorized, with use cases) { "status": "owned", - "matched_registration": { "registry": "USPTO", "number": "9999999", "mark": "SWOOSH", "status": "active" }, - "license_type": "owned", + "matched_registration": { "registry": "USPTO", "number": "9999999", "mark": "SWOOSH", "registration_status": "active" }, "countries": ["US"], "nice_classes": [25, 41], "use_case_authorization": { @@ -111,6 +120,25 @@ Trademark facts are largely public-record (registry filings are open). The agent "status": "not_ours" } ``` + +```json Response (transferring) +{ + "status": "transferring", + "matched_registration": { "registry": "USPTO", "number": "1234567", "mark": "AIR JORDAN", "registration_status": "active" }, + "context_note": "Mark transferring to spinoff entity on 2026-08-15." +} +``` + +```json Response (error) +{ + "errors": [ + { + "code": "AMBIGUOUS_MATCH", + "message": "Multiple registrations match 'CONVERSE'; narrow with registry, number, or countries." + } + ] +} +``` ## Status semantics for trademarks @@ -118,28 +146,31 @@ Trademark facts are largely public-record (registry filings are open). The agent | Status | Meaning | |---|---| | `owned` | The brand holds the registration. | -| `licensed_in` | The brand uses the mark under license from another entity (`licensor_domain` populated). | +| `licensed_in` | The brand uses the mark under license (`licensor_domain` populated). | | `licensed_out` | The brand licenses the mark to another entity. | -| `disputed` | The brand actively contests this registration (e.g., another party registered the mark in a jurisdiction the brand intends to challenge). | +| `transferring` | Mark transferring to another party (M&A, licensing transition, spinoff). | +| `disputed` | The brand actively contests this registration. | | `not_ours` | The brand has no relationship to this registration. | -| `unknown` | The agent cannot match the input to any registration. Caller MAY fall back to public registry crawls. | +| `unknown` | Agent cannot match the input. Caller MAY fall back to registry crawl. | -`pending_review` is uncommon for trademarks — registration is a public-record event with definitive ownership at any given time. Agents MAY return it for marks under M&A or licensing transition. +`pending_review` is not applicable — trademark registrations are public-record events with definitive ownership at any given time. ## Trust model -Identical to [`verify_subsidiary_claim`](/docs/brand-protocol/tasks/verify_subsidiary_claim#trust-model). The agent's signed response is authoritative; crawl of public trademark registries is the fallback. When the agent and registry disagree (rare but real — e.g., a mark recently transferred but not yet reflected in the registry's public records), the agent wins because the brand is the authority over its current licensing relationships. +The agent's response is **authoritative** when signed under the brand's `adcp_use: "response-signing"` JWK. Conflict resolution mirrors [`verify_subsidiary_claim` § Trust model](/docs/brand-protocol/tasks/verify_subsidiary_claim#trust-model). When the agent and the registry disagree (rare — e.g., a mark recently transferred but not yet reflected in the registry's public records), the agent wins because the brand is the authority over its current licensing relationships. ## Caching -Trademark registration data is stable; licensing relationships less so: - - `owned` / `not_ours` / `disputed` — stable. 24-72h cache. - `licensed_in` / `licensed_out` — moderately volatile. 24h cache; re-check on contract-affected workflows. -- `use_case_authorization` — most volatile field. Re-check per session. +- `transferring` — volatile until transition. Max-age ≤4h. +- `use_case_authorization` — most volatile. Re-check per session. ## Error handling -See [`verify_subsidiary_claim` § Error handling](/docs/brand-protocol/tasks/verify_subsidiary_claim#error-handling). Additional error codes specific to trademarks: - -- `AMBIGUOUS_MATCH` — multiple registrations match the input (typically when neither `registry` nor `number` is provided and the mark exists in several jurisdictions). Caller should narrow the query with `registry`, `number`, or `countries`. +| Error code | Cause | +|---|---| +| `AUTH_INVALID` | Caller's signed envelope did not verify. | +| `RATE_LIMITED` | Agent has rate-limited the caller. Agents SHOULD return `Retry-After` and prefer cached prior answer over hard error. | +| `INVALID_INPUT` | Required field missing or fails the agent's input policy. | +| `AMBIGUOUS_MATCH` | Multiple registrations match the input (typically when neither `registry` nor `number` is provided and the mark exists in several jurisdictions). Caller should narrow with `registry`, `number`, or `countries`. | diff --git a/static/schemas/source/brand/verification-status.json b/static/schemas/source/brand/verification-status.json index 6d0f1d4e5b..51160621b6 100644 --- a/static/schemas/source/brand/verification-status.json +++ b/static/schemas/source/brand/verification-status.json @@ -7,6 +7,7 @@ "enum": [ "owned", "pending_review", + "transferring", "disputed", "not_ours", "licensed_in", @@ -15,7 +16,8 @@ ], "enumDescriptions": { "owned": "Definitively belongs to this brand, currently.", - "pending_review": "The brand is aware of this claim and has not yet decided. Consumers SHOULD NOT extend governance trust through pending claims; treating them as 'will-likely-be-confirmed' is also wrong — they may still be disputed.", + "pending_review": "The brand is aware of this claim and has not yet decided. When returning this status, the agent MUST also return `expected_resolution_window_days` and MUST transition the claim to a terminal status (owned/disputed/not_ours) or flip to `unknown` once the window elapses. Consumers SHOULD NOT extend governance trust through pending claims.", + "transferring": "Ownership is provably changing — M&A in flight, divestiture closing, or a known imminent transition. Distinct from `pending_review` (under-review-by-us); `transferring` signals 'the answer is known to be becoming something else.' Consumers SHOULD treat as `owned` for stability until the agent moves to the new state, but MAY surface the in-flight state in UIs.", "disputed": "The brand actively rejects this claim. Consumers MUST treat the claim as invalid and SHOULD surface the dispute (e.g., 'X says this is not theirs').", "not_ours": "The brand affirms it is not their property / subsidiary / mark. Equivalent to 'disputed' but used when the brand-agent has no record of an existing claim — a clean 'we do not own this.'", "licensed_in": "The brand uses this asset under license from another entity. The response carries `licensor_domain` when this status is returned. Applies to trademarks and (rarely) properties.", diff --git a/static/schemas/source/brand/verify-property-response.json b/static/schemas/source/brand/verify-property-response.json index 9eca8b39cf..4bba6985fc 100644 --- a/static/schemas/source/brand/verify-property-response.json +++ b/static/schemas/source/brand/verify-property-response.json @@ -14,13 +14,14 @@ "title": "VerifyPropertySuccess", "properties": { "status": { - "$ref": "/schemas/brand/verification-status.json", - "description": "Verification status. Applicable values for this tool: owned, disputed, not_ours, unknown. (pending_review and licensed_in/out are uncommon for properties; agents MAY return them but most properties have a definitive yes/no answer.)" + "type": "string", + "enum": ["owned", "transferring", "disputed", "not_ours", "unknown"], + "description": "Verification status. Subset of /schemas/brand/verification-status.json applicable to property claims. `pending_review` is excluded because property ownership is generally definitive — agents that need to surface in-process state SHOULD use `transferring`. `licensed_in` / `licensed_out` are excluded because licensing applies to trademarks and brands, not the underlying digital property." }, "relationship": { "type": "string", "enum": ["owned", "direct", "delegated", "ad_network"], - "description": "Public — when status is `owned`, the commercial relationship between the brand and the property. Mirrors brand.json's properties[].relationship enum. Omitted when status is not `owned`." + "description": "Public — when status is `owned` or `transferring`, the commercial relationship between the brand and the property. Mirrors brand.json's properties[].relationship enum. Omitted when status is not in that set." }, "brand_id": { "type": "string", @@ -30,22 +31,25 @@ "regions": { "type": "array", "items": { "type": "string" }, - "description": "Public — ISO 3166-1 alpha-2 country codes where this property is the brand's primary/canonical surface. Empty array = global / no regional restriction." + "description": "Public — ISO 3166-1 alpha-2 country codes where this property is the brand's primary/canonical surface. Use the sentinel `\"global\"` (matching the request schema's region field) to signal no regional restriction. Omitting the field is equivalent to `unknown regions`." }, "use_case_authorization": { "type": "object", - "description": "Authorized-tier only. Per-use-case authorization flags. The agent's answer to whether the named use case is permitted on this property — distinct from ownership.", + "description": "Authorized-tier only. Per-use-case authorization flags. The agent's answer to whether the named use case is permitted on this property — distinct from ownership. Registered keys: advertising, endorsement, retail_listing, editorial, commercial_advertising, merchandise_resale. Agents MAY add additional keys (additionalProperties: boolean).", "properties": { "advertising": { "type": "boolean", "description": "Programmatic advertising allowed via AdCP." }, "endorsement": { "type": "boolean", "description": "Use of the property in endorsement deals." }, - "retail_listing": { "type": "boolean", "description": "Listing in retail / commerce contexts." } + "retail_listing": { "type": "boolean", "description": "Listing in retail / commerce contexts." }, + "editorial": { "type": "boolean", "description": "Editorial mentions and reviews." }, + "commercial_advertising": { "type": "boolean", "description": "Paid advertising placements off-AdCP." }, + "merchandise_resale": { "type": "boolean", "description": "Resale of merchandise associated with the property." } }, "additionalProperties": { "type": "boolean" } }, "context_note": { "type": "string", "maxLength": 500, - "description": "Public — free-text context the brand chooses to surface (e.g., 'Regional CN site', 'Legacy domain, redirects to nike.com')." + "description": "Public — free-text context the brand chooses to surface (e.g., 'Regional CN site', 'Legacy domain, redirects to nike.com'). When status is `disputed`, carries the rationale." }, "context": { "$ref": "/schemas/core/context.json" @@ -55,7 +59,12 @@ } }, "required": ["status"], - "additionalProperties": true + "additionalProperties": true, + "not": { + "anyOf": [ + { "required": ["errors"] } + ] + } }, { "title": "VerifyPropertyError", diff --git a/static/schemas/source/brand/verify-subsidiary-claim-response.json b/static/schemas/source/brand/verify-subsidiary-claim-response.json index 12e350744c..6b13267787 100644 --- a/static/schemas/source/brand/verify-subsidiary-claim-response.json +++ b/static/schemas/source/brand/verify-subsidiary-claim-response.json @@ -14,12 +14,13 @@ "title": "VerifySubsidiaryClaimSuccess", "properties": { "status": { - "$ref": "/schemas/brand/verification-status.json", - "description": "Verification status. Applicable values for this tool: owned, pending_review, disputed, not_ours, unknown. (licensed_in / licensed_out do not apply to subsidiaries.)" + "type": "string", + "enum": ["owned", "pending_review", "transferring", "disputed", "not_ours", "unknown"], + "description": "Verification status. Subset of /schemas/brand/verification-status.json applicable to subsidiary claims; `licensed_in` / `licensed_out` are excluded because subsidiaries aren't licensed (brands and trademarks are)." }, "brand_id": { "type": "string", - "description": "Public — the house's brand_id for this subsidiary when status is `owned` or `pending_review`. Lets the caller cross-reference with brand_refs[] / brands[] entries.", + "description": "Public — the house's brand_id for this subsidiary when status is `owned`, `pending_review`, or `transferring`. Lets the caller cross-reference with brand_refs[] / brands[] entries.", "pattern": "^[a-z0-9_]+$" }, "first_observed_by_house_at": { @@ -30,12 +31,12 @@ "expected_resolution_window_days": { "type": "integer", "minimum": 0, - "description": "Authorized-tier only. The house's expected window for resolving a `pending_review` claim. Aggregate signal, not a guarantee; does not expose queue position or internal operations." + "description": "Authorized-tier only when populated alongside a terminal status. REQUIRED when status is `pending_review`. The house's expected window for resolving a `pending_review` claim. Agents MUST transition the claim to a terminal status or to `unknown` once the window elapses — staleness past the window is a contract violation. Aggregate signal; does not expose queue position or internal ops." }, - "dispute_reason": { + "context_note": { "type": "string", "maxLength": 500, - "description": "Public — when status is `disputed`, a short human-readable rationale (e.g., 'Trademark conflict; not affiliated'). May be empty even on disputed." + "description": "Public — free-text context the brand chooses to surface. When status is `disputed`, this carries the rationale (e.g., 'Trademark conflict; not affiliated'). When status is `transferring`, the in-flight transition's nature (e.g., 'Divestiture closing 2026-08'). May be empty." }, "context": { "$ref": "/schemas/core/context.json" @@ -45,7 +46,12 @@ } }, "required": ["status"], - "additionalProperties": true + "additionalProperties": true, + "not": { + "anyOf": [ + { "required": ["errors"] } + ] + } }, { "title": "VerifySubsidiaryClaimError", diff --git a/static/schemas/source/brand/verify-trademark-response.json b/static/schemas/source/brand/verify-trademark-response.json index 03d4b1342b..49215e10ec 100644 --- a/static/schemas/source/brand/verify-trademark-response.json +++ b/static/schemas/source/brand/verify-trademark-response.json @@ -14,29 +14,25 @@ "title": "VerifyTrademarkSuccess", "properties": { "status": { - "$ref": "/schemas/brand/verification-status.json", - "description": "Verification status. Applicable values for this tool: owned, licensed_in, licensed_out, disputed, not_ours, unknown. (pending_review is uncommon — trademark registrations are public-record events with definitive ownership at any given time.)" + "type": "string", + "enum": ["owned", "licensed_in", "licensed_out", "transferring", "disputed", "not_ours", "unknown"], + "description": "Verification status. Subset of /schemas/brand/verification-status.json applicable to trademark claims. `pending_review` is excluded — trademark registrations are public-record events with definitive ownership at any given time; mid-transition cases use `transferring`." }, "matched_registration": { "type": "object", - "description": "Public — when status is `owned`, `licensed_in`, or `licensed_out`, the registration the agent matched the query to. Same shape as brand.json's #/definitions/trademark.", + "description": "Public — when status is `owned`, `licensed_in`, `licensed_out`, or `transferring`, the registration the agent matched the query to. Same shape as brand.json's #/definitions/trademark.", "properties": { "registry": { "type": "string" }, "number": { "type": "string" }, "mark": { "type": "string" }, - "status": { + "registration_status": { "type": "string", "enum": ["active", "pending", "abandoned", "cancelled", "expired"], - "description": "Registration status from the registry, distinct from this response's outer `status` field." + "description": "Registration status from the registry, distinct from this response's outer `status` field. Renamed from `status` on the brand.json definition to avoid collision with the verification status above." } }, "additionalProperties": true }, - "license_type": { - "type": "string", - "enum": ["owned", "licensed_in", "licensed_out"], - "description": "Public — licensing relationship when status is `owned`, `licensed_in`, or `licensed_out`. Redundant with status for owned/licensed_in/licensed_out; kept for parity with brand.json #/definitions/trademark." - }, "licensor_domain": { "type": "string", "format": "hostname", @@ -54,13 +50,21 @@ }, "use_case_authorization": { "type": "object", - "description": "Authorized-tier only. Per-use-case permission for this mark.", + "description": "Authorized-tier only. Per-use-case permission for this mark. Registered keys: advertising, endorsement, retail_listing, editorial, commercial_advertising, merchandise_resale. Agents MAY add additional keys (additionalProperties: boolean).", + "properties": { + "advertising": { "type": "boolean" }, + "endorsement": { "type": "boolean" }, + "retail_listing": { "type": "boolean" }, + "editorial": { "type": "boolean" }, + "commercial_advertising": { "type": "boolean" }, + "merchandise_resale": { "type": "boolean" } + }, "additionalProperties": { "type": "boolean" } }, "context_note": { "type": "string", "maxLength": 500, - "description": "Public — free-text context the brand chooses to surface." + "description": "Public — free-text context the brand chooses to surface. When status is `disputed`, carries the rationale." }, "context": { "$ref": "/schemas/core/context.json" @@ -70,7 +74,12 @@ } }, "required": ["status"], - "additionalProperties": true + "additionalProperties": true, + "not": { + "anyOf": [ + { "required": ["errors"] } + ] + } }, { "title": "VerifyTrademarkError", From eee1102efda7f99f7734e519503401698b881a87 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Thu, 14 May 2026 10:47:20 -0400 Subject: [PATCH 3/5] chore(brand-protocol): accept three new verify_* response oneOfs into baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI check test:oneof-discriminators flagged the three new verification response schemas as undiscriminated oneOfs. They follow the same shape as the existing brand-protocol responses already in the baseline (get-brand-identity-response, search-brands-response, etc.) — success arm required:[status] vs error arm required:[errors] — and don't have a natural cross-arm discriminator beyond the existing required-field asymmetry. Ratcheting the baseline via --update --accept-new is the documented path for this pattern. The three entries: - brand/verify-property-response.json - brand/verify-subsidiary-claim-response.json - brand/verify-trademark-response.json Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/oneof-discriminators.baseline.json | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/scripts/oneof-discriminators.baseline.json b/scripts/oneof-discriminators.baseline.json index 146893d7cb..a0da62bde2 100644 --- a/scripts/oneof-discriminators.baseline.json +++ b/scripts/oneof-discriminators.baseline.json @@ -38,8 +38,8 @@ }, "brand.json##/oneOf": { "kind": "dangerous", - "variants": 4, - "note": "shared-required=[∅]; 0:req=[authoritative_location] | 1:req=[house] | 2:req=[∅] | 3:req=[house,brands]" + "variants": 5, + "note": "shared-required=[∅]; 0:req=[authoritative_location] | 1:req=[house] | 2:req=[∅] | 3:req=[house] | 4:req=[∅]" }, "brand/acquire-rights-response.json##/oneOf": { "kind": "dangerous", @@ -76,6 +76,21 @@ "variants": 2, "note": "0:[rights_id,terms] | 1:[errors]" }, + "brand/verify-property-response.json##/oneOf": { + "kind": "narrowable", + "variants": 2, + "note": "0:[status] | 1:[errors]" + }, + "brand/verify-subsidiary-claim-response.json##/oneOf": { + "kind": "narrowable", + "variants": 2, + "note": "0:[status] | 1:[errors]" + }, + "brand/verify-trademark-response.json##/oneOf": { + "kind": "narrowable", + "variants": 2, + "note": "0:[status] | 1:[errors]" + }, "compliance/comply-test-controller-response.json##/oneOf": { "kind": "dangerous", "variants": 7, From 25f6cace4d15d429c610554e29c23a965c9b495c Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Thu, 14 May 2026 13:26:36 -0400 Subject: [PATCH 4/5] docs(brand-protocol): reframe verification as prerequisite gate; clarify aging contract enforcement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two pieces of feedback addressed. Verification as a gate, not a signal - verify_property and verify_subsidiary_claim now lead with "this is a prerequisite gate — check before you proceed, not after." The gating semantics are what justify the round-trip cost; consuming the answer post-decision means you're using the wrong tool. - When-to-use sections reframed around gating points: inventory onboarding (gate the catalog entry), creative clearance (gate approval), fraud escalation (gate the action), brand-relationship establishment (gate governance trust extension). - verify_trademark already framed correctly; minor lead tightening. pending_review aging contract honesty - MUST language stays — declared intent + defined fallback is better than today's silent pending limbo. But the enforcement story now reads as it actually is: agent-side, not spec-level. Most brand-side day-one deployments won't ship automated pending→unknown flipping. - New dedicated subsection in the RFC: "pending_review aging and the crawl safety net." Spells out: enforcement is agent-side; day-one deployments will lag; consumer-side fallback to crawl on stale pending_review IS the safety net; MUST is fine because the fallback is graceful. - Trust-model row updated to instruct consumers to treat stale pending_review as effectively unknown. - Resolved-decision #2 points at the new section. - verify_subsidiary_claim task page mirrors the same framing. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../proposals/brand-verification-rfc.mdx | 31 ++++++++++++++----- docs/brand-protocol/tasks/verify_property.mdx | 14 ++++++--- .../tasks/verify_subsidiary_claim.mdx | 10 +++--- .../brand-protocol/tasks/verify_trademark.mdx | 2 +- 4 files changed, 39 insertions(+), 18 deletions(-) diff --git a/docs/brand-protocol/proposals/brand-verification-rfc.mdx b/docs/brand-protocol/proposals/brand-verification-rfc.mdx index f8752038f2..8b58ba835d 100644 --- a/docs/brand-protocol/proposals/brand-verification-rfc.mdx +++ b/docs/brand-protocol/proposals/brand-verification-rfc.mdx @@ -49,7 +49,7 @@ The status enum captures the rich state the crawl model can't express: | Status | Meaning | |---|---| | `owned` | Definitively belongs to this brand, currently. | -| `pending_review` | Claim known to the brand; no decision yet. **MUST be returned only with `expected_resolution_window_days`**, and the agent MUST transition to a terminal status or `unknown` once the window elapses. Without aging, `pending_review` becomes a polite shrug. | +| `pending_review` | Claim known to the brand; no decision yet. **MUST be returned with `expected_resolution_window_days`**; the agent MUST transition to a terminal status or `unknown` once the window elapses. Enforcement is agent-side (not spec-level) — see [pending_review aging](#pending_review-aging-and-the-crawl-safety-net) below. | | `transferring` | Ownership is provably changing — M&A in flight, divestiture closing, known imminent transition. Distinct from `pending_review` (under-review-by-us); `transferring` signals "the answer is known to be becoming something else." | | `disputed` | The brand actively rejects this claim. | | `not_ours` | The brand affirms it is not their property / subsidiary / mark. | @@ -61,6 +61,8 @@ Each tool's response schema **constrains** the enum to the statuses applicable t ### `verify_subsidiary_claim` +**Like `verify_property`, this is a gate before you proceed.** Inventory onboarding, brand-relationship establishment, creative clearance, member-feature provisioning — anywhere a downstream system would extend governance trust through a brand → house relationship, this call gates that step. You check the claim with the named house and act on the answer, you don't post-hoc reconcile a state after the fact. + A consumer detects a leaf-only edge: `converse.com` claims `house_domain: "nikeinc.com"`, but Nike's `brand_refs[]` doesn't include it yet. Today the consumer is told to email `contact.email` and wait. With this tool: ```json @@ -93,13 +95,15 @@ The agent's signed response is authoritative. The crawl backstop becomes unneces ### `verify_property` -This is **not a bid-time tool.** Sub-100ms auction budgets don't permit MCP round-trips. `verify_property` slots in at: +**This is a prerequisite check, not a mid-flight signal.** Verification gates the *next step* in a workflow: you check before you proceed, not after. Sub-100ms auction budgets don't permit MCP round-trips, so this is never bid-time. It runs at decision points where you can still say no: + +- **Inventory onboarding** — an SSP adds a property to its catalog; the buyer requires brand confirmation before allowing bidding on it. The verification gates the catalog entry. +- **Creative clearance** — a creative-approval workflow encounters a property reference; verification gates approval. +- **Fraud detection** — a domain is suspected of falsely claiming brand affiliation; verification gates whatever escalation path follows (block, flag, investigate). -- **Supply-path curation** — when an SSP onboards a new property as inventory and the buyer wants the brand's say-so before allowing bidding on it. -- **Fraud detection** — when a domain is suspected of falsely claiming brand affiliation. -- **Creative clearance** — when a creative-approval workflow encounters a property reference. +The gating semantics are what justifies the round-trip cost. If you're consuming the answer after a decision is locked in, you're using the wrong tool — `brand.json` properties[] inference is the read-only signal for that case. -Cache the answer (24-72h is appropriate for `owned`/`not_ours`); re-check periodically. +Cache the answer (24-72h is appropriate for `owned`/`not_ours`); re-check at the next gate, not every bid. ```json // Request @@ -201,7 +205,7 @@ The agent's response is **authoritative**: signed under the brand's `adcp_use: " | Agent says | Crawl observes | Consumer treats as | |---|---|---| | `owned` | Mutual or leaf-only | Trusted edge. Governance propagation: yes. | -| `pending_review` | Leaf-only (parent silent) | Trusted leaf identity; relationship trust withheld until window elapses or status transitions. | +| `pending_review` | Leaf-only (parent silent) | Trusted leaf identity; relationship trust withheld. When the response's `expected_resolution_window_days` is exceeded without transition, consumer SHOULD treat as if `unknown` returned and fall back to crawl-based inference. | | `transferring` | Any | Trusted but in-flight; consumers MAY treat as `owned` for stability and SHOULD surface the transition in UI. | | `disputed` | Leaf-only or mutual | The brand has spoken. **Leaf is treated as having a rejected parent claim.** UI: "Brand X says this is not theirs." | | `not_ours` | Leaf-only or mutual | The brand affirms no relationship. **Leaf treated as standalone with a disputed parent claim, regardless of leaf's `house_domain` declaration.** UI surfaces the rejection. | @@ -224,6 +228,17 @@ Verification responses SHOULD be cacheable per standard HTTP semantics. Agents S Consumers MAY override agent-supplied cache hints downward. A consumer SHOULD NOT cache beyond the agent's supplied `max-age`. +## `pending_review` aging and the crawl safety net + +The aging contract — `pending_review` MUST come with `expected_resolution_window_days` AND the agent MUST transition to a terminal status (`owned` / `disputed` / `not_ours`) or to `unknown` once the window elapses — reads cleanly as a normative MUST. The enforcement story is more honest: + +- **Enforcement is agent-side, not spec-level.** No external party validates the aging contract. The protocol can't compel a brand's portfolio team to action a queue, and the spec doesn't try. +- **Day-one deployments will lag.** Most brand-side implementations (especially self-hosted holdcos) won't ship with automated `pending_review → unknown` flipping on launch. Portfolio teams operate at human-week timescales; flipping stale claims is not the first automation they build. +- **The safety net is the crawl fallback, not the spec.** When an agent returns stale `pending_review` past its declared window, the **consumer** treats the response as effectively `unknown` and falls back to crawl-based mutual-assertion inference. This is the same fallback consumers use when the agent is unreachable or genuinely returns `unknown` — it requires nothing new on the consumer side. +- **MUST is the right normative language anyway.** It states declared intent and a defined fallback. That's better than the status quo, where `pending` limbo has no expiry and no recourse. A noisy `pending_review` from an under-automated agent is recoverable; silence isn't. + +In summary: agents SHOULD transition or flip past the window; consumers MUST tolerate agents that don't, by falling back to crawl. The aging contract is real, the enforcement is realistic, the fallback is graceful. + ## Rate limiting (normative expectation) Agents MAY rate-limit per `{caller_identity, tool, query_target}`. Agents SHOULD: @@ -312,7 +327,7 @@ The [Conformance](/docs/brand-protocol/brand-json#conformance) update is one SHO The following were open during draft and are now resolved as the spec contract: 1. **`verify_property.use_case` field.** Kept. Mirrors the `get_brand_identity` `use_case` precedent. -2. **`pending_review` queue position vs aggregate window.** Aggregate only (`expected_resolution_window_days`). Queue position leaks operational state. Additionally: `pending_review` MUST come with the window AND the agent MUST transition or flip to `unknown` past the window — an aging contract, not a polite shrug. +2. **`pending_review` queue position vs aggregate window.** Aggregate only (`expected_resolution_window_days`). Queue position leaks operational state. Additionally: `pending_review` MUST come with the window AND the agent MUST transition or flip to `unknown` past the window. Enforcement is agent-side, not spec-level; the safety net is consumer-side fallback to crawl on stale `pending_review` responses. See [`pending_review` aging and the crawl safety net](#pending_review-aging-and-the-crawl-safety-net). 3. **Agent says `not_ours` while leaf publishes `house_domain` claim.** Normative: agent wins. See [Trust model § conflict resolution](#trust-model). UI guidance for leaf's recourse is a follow-up issue. 4. **Rate-limiting.** Agent's call; spec sets the expectation of per-`{caller, tool, target}`. Agents SHOULD return `Retry-After` on limit and SHOULD prefer cached-prior-answer over `RATE_LIMITED` error. 5. **Bulk variants** (`verify_subsidiary_claims`, plural). Deferred. Filed as follow-up — crawlers will demand it within 6 months; v1 ships single-target shape. diff --git a/docs/brand-protocol/tasks/verify_property.mdx b/docs/brand-protocol/tasks/verify_property.mdx index 7c26284cc7..bbc8acd33f 100644 --- a/docs/brand-protocol/tasks/verify_property.mdx +++ b/docs/brand-protocol/tasks/verify_property.mdx @@ -9,7 +9,7 @@ testable: true **Proposed (RFC).** This task is part of the [brand verification RFC](/docs/brand-protocol/proposals/brand-verification-rfc) and not yet normative. -Ask a brand-agent whether a property (website, app, podcast, etc.) belongs to this brand. Used in **inventory onboarding / supply-path-curation workflows**, fraud detection, and creative-clearance — not at bid time (sub-100ms auction budgets don't accommodate MCP round-trips). Cache and re-validate periodically. +Ask a brand-agent whether a property (website, app, podcast, etc.) belongs to this brand. **This is a prerequisite gate — check before you proceed, not a signal you consume after a decision is locked in.** Used at inventory onboarding, creative clearance, and fraud-escalation gates. Never at bid time (sub-100ms auction budgets don't accommodate MCP round-trips). Cache and re-validate at the next gate. ## Schema @@ -29,11 +29,15 @@ The brand-agent advertises this task in its `get_adcp_capabilities` response (al ## When to use -- **Supply-path curation.** An SSP onboards a property as inventory and the buyer wants the brand's say-so before allowing bidding on it. -- **Fraud detection.** A domain is suspected of falsely claiming brand affiliation. -- **Creative clearance.** A creative-approval workflow encounters a property reference and needs ownership confirmation. +Verification gates the *next step* in a workflow. Concrete gating points: -Not bid-time. Cache 24-72h for `owned`/`not_ours`; less for `transferring`/`pending` states. +- **Inventory onboarding.** An SSP adds a property to its catalog; verification gates the catalog entry before the property goes live for bidding. +- **Creative clearance.** A creative-approval workflow encounters a property reference; verification gates approval. +- **Fraud escalation.** A domain is suspected of falsely claiming brand affiliation; verification gates the chosen escalation path (block, flag, investigate). + +If the answer is "consumed informationally after the decision is already locked in," you're using the wrong tool — read `brand.json` `properties[]` directly. The gate-before-proceed semantics are what justify the MCP round-trip. + +Cache 24-72h for `owned`/`not_ours`; less for `transferring`. Re-check at the next gate, not at every bid. ## Authorization tiers diff --git a/docs/brand-protocol/tasks/verify_subsidiary_claim.mdx b/docs/brand-protocol/tasks/verify_subsidiary_claim.mdx index 725190f4c6..6d0890320a 100644 --- a/docs/brand-protocol/tasks/verify_subsidiary_claim.mdx +++ b/docs/brand-protocol/tasks/verify_subsidiary_claim.mdx @@ -9,7 +9,7 @@ testable: true **Proposed (RFC).** This task is part of the [brand verification RFC](/docs/brand-protocol/proposals/brand-verification-rfc) and not yet normative. The crawl-based [mutual-assertion model](/docs/brand-protocol/brand-json#mutual-assertion-trust-model) remains authoritative until the RFC ratifies. -Ask a brand-agent whether the named brand is a subsidiary of this house. Returns an authoritative status from the brand's own agent, replacing crawl-based inference when the brand-agent supports this task. +Ask a brand-agent whether the named brand is a subsidiary of this house. **This is a prerequisite gate — check before you proceed.** Returns an authoritative status from the brand's own agent, replacing crawl-based inference when the brand-agent supports this task. ## Schema @@ -34,7 +34,9 @@ Callers MUST check capability advertisement before relying on this task; brand-a ## When to use -The crawl-based mutual-assertion model — leaf publishes `house_domain: X`, X's `brand_refs[]` includes leaf, both halves verified by independent fetch — covers the common case. But it has known gaps: +Verification gates the *next step* in a workflow. Call this where you would otherwise extend governance trust through a brand → house relationship: inventory onboarding, brand-relationship establishment, member-feature provisioning, creative clearance that depends on parent-brand authorization. Check the claim with the named house, act on the answer — don't reconcile state after the fact. + +The crawl-based mutual-assertion model — leaf publishes `house_domain: X`, X's `brand_refs[]` includes leaf, both halves verified by independent fetch — covers the common case but has gaps this tool closes: - **Two-state visibility.** Crawl answers "mutual" or "not mutual." It can't surface *pending review*, *transferring*, or *disputed*. - **TTL-bound freshness.** A consumer's view is only as fresh as its last crawl of both sides. @@ -128,7 +130,7 @@ Conflict resolution between agent response and crawl observation (normative — | Agent says | Crawl says | Consumer treats as | |---|---|---| | `owned` | Mutual or leaf-only | Trusted edge. Governance propagation: yes. | -| `pending_review` | Leaf-only (parent silent) | Trusted leaf identity; relationship trust withheld until window elapses or status transitions. UI: "Pending parent confirmation, expected within N days." | +| `pending_review` | Leaf-only (parent silent) | Trusted leaf identity; relationship trust withheld. When the response's `expected_resolution_window_days` is exceeded without transition, consumer SHOULD treat as if `unknown` returned and fall back to crawl. UI: "Pending parent confirmation, expected within N days." | | `transferring` | Any | Trusted but in-flight; consumers MAY treat as `owned` for stability and SHOULD surface the transition in UI. | | `disputed` | Leaf-only or mutual | The brand has spoken. Treat leaf as having a rejected parent claim. UI: "Brand X says this is not theirs." | | `not_ours` | Leaf-only | Leaf treated as standalone with disputed parent claim. | @@ -136,7 +138,7 @@ Conflict resolution between agent response and crawl observation (normative — When the agent answers, the agent wins. The crawl path remains the fallback only when the agent is unreachable, returns `unknown`, or doesn't implement the task. -`pending_review` requires the agent to set `expected_resolution_window_days` AND transition the claim to a terminal status (or `unknown`) once the window elapses. Staleness past the window is a contract violation; consumers MAY treat such responses as `unknown`. +`pending_review` requires the agent to set `expected_resolution_window_days` AND transition the claim to a terminal status (or `unknown`) once the window elapses. **Enforcement is agent-side, not spec-level** — most day-one brand deployments won't have the automation to auto-flip stale claims. The safety net is consumer-side: when a `pending_review` response is older than its declared window, consumers SHOULD treat the answer as effectively `unknown` and fall back to crawl-based inference (same fallback used when the agent is unreachable). The aging contract is a declared intent with a defined fallback, not an externally-enforced guarantee. See [RFC § pending_review aging](/docs/brand-protocol/proposals/brand-verification-rfc#pending_review-aging-and-the-crawl-safety-net). ## Caching diff --git a/docs/brand-protocol/tasks/verify_trademark.mdx b/docs/brand-protocol/tasks/verify_trademark.mdx index 1c674792d8..0b799fd517 100644 --- a/docs/brand-protocol/tasks/verify_trademark.mdx +++ b/docs/brand-protocol/tasks/verify_trademark.mdx @@ -29,7 +29,7 @@ The brand-agent advertises this task in its `get_adcp_capabilities` response: ## When to use -Trademark ownership has gnarly edges that static publication and registry crawls can't always resolve: +**This is a creative-clearance gate.** Call it where a workflow about to apply a trademark to a creative needs the brand's authoritative go/no-go — not after the creative is already approved. Trademark ownership has gnarly edges that static publication and registry crawls can't always resolve: - **Cross-jurisdiction conflicts.** USPTO `CONVERSE` and EUIPO `CONVERSE` may be held by different parties. - **Nice class disambiguation.** `DELTA` (airline, class 39) and `DELTA` (faucet, class 11) are different brands with the same mark. From 7643b0265c5b4f0922d7e942b50044112b7ecda9 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Thu, 14 May 2026 13:37:22 -0400 Subject: [PATCH 5/5] chore(brand-protocol): allowlist canonical app-store values on verify_property request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Platform-agnosticism lint flagged the store enum in brand/verify-property-request.json (apple/google/amazon/roku) because the new file path doesn't match the existing brand.json allowlist entries. Same canonical-identifier justification applies: these are the names of the app stores, not vendor-promotional values. Factoring the store enum into a shared enums/property-store.json (referenced from both brand.json and the verify_property request) is the right long-term cleanup — protocol-expert review flagged it on PR #4540. Tracking separately; for now the allowlist mirrors the existing brand.json pattern. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/check-platform-agnostic.cjs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/check-platform-agnostic.cjs b/tests/check-platform-agnostic.cjs index f25583e418..54f5796810 100644 --- a/tests/check-platform-agnostic.cjs +++ b/tests/check-platform-agnostic.cjs @@ -77,6 +77,14 @@ const ENUM_VALUE_ALLOWLIST = [ { value: 'amazon', pathContains: 'brand.json' }, { value: 'roku', pathContains: 'brand.json' }, + // brand/verify-property-request.json — store enum mirrors brand.json's + // properties[].store. Factoring into a shared enums/property-store.json + // is tracked as cleanup; same canonical-identifier justification applies. + { value: 'apple', pathContains: 'brand/verify-property-request.json' }, + { value: 'google', pathContains: 'brand/verify-property-request.json' }, + { value: 'amazon', pathContains: 'brand/verify-property-request.json' }, + { value: 'roku', pathContains: 'brand/verify-property-request.json' }, + // brand.json — feed_format: google_merchant_center and facebook_catalog are // widely-adopted open interchange formats implemented by many third parties. { value: 'google_merchant_center', pathContains: 'brand.json' },