From 036c874ad3f91738b0186b5241b69ef0baed9d6d Mon Sep 17 00:00:00 2001 From: Emma Mulitz Date: Wed, 13 May 2026 21:52:47 -0400 Subject: [PATCH 1/3] feat(registry): expose membership tier on public operator lookups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /api/registry/operator?domain=X now surfaces the queried member's AAO membership tier when the profile owner has opted their member card into public visibility (is_public=true). The member object grows two optional fields: membership_tier (raw enum, e.g. company_leader) and membership_tier_label (human-readable, e.g. Leader). Private profiles still return only { slug, display_name } — tier reflects billing state, so we follow the existing profile-card visibility toggle rather than introducing a second one. The fields are absent (not null) for private profiles and for orgs without a resolvable tier, so existing consumers see no shape change. --- .../operator-member-tier-public-card.md | 12 ++++++ server/src/routes/registry-api.ts | 31 +++++++++++++-- server/src/schemas/registry.ts | 8 ++++ .../unit/operator-publisher-lookup.test.ts | 32 ++++++++++++++++ static/openapi/registry.yaml | 38 +++++++++++++++++++ 5 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 .changeset/operator-member-tier-public-card.md diff --git a/.changeset/operator-member-tier-public-card.md b/.changeset/operator-member-tier-public-card.md new file mode 100644 index 0000000000..e3b66d20f6 --- /dev/null +++ b/.changeset/operator-member-tier-public-card.md @@ -0,0 +1,12 @@ +--- +--- + +`/api/registry/operator?domain=X` now surfaces the queried member's AAO +membership tier when the profile owner has opted their member card into public +visibility (`is_public=true`). The `member` object grows two optional fields: +`membership_tier` (raw enum, e.g. `company_leader`) and `membership_tier_label` +(human-readable, e.g. `Leader`). Private profiles still return only +`{ slug, display_name }` — tier reflects billing state and we follow the +profile-card visibility toggle rather than introducing a second one. The +fields are absent (not `null`) for private profiles and for orgs without a +resolvable tier so existing consumers see no shape change. diff --git a/server/src/routes/registry-api.ts b/server/src/routes/registry-api.ts index b83cd5b406..095300ce72 100644 --- a/server/src/routes/registry-api.ts +++ b/server/src/routes/registry-api.ts @@ -31,7 +31,7 @@ import { import { getPublicJwks } from "../services/verification-token.js"; import { renderBadgeSvg, VALID_BADGE_ROLES } from "../services/badge-svg.js"; import { runBadgeFanOut } from "../services/badge-issuance.js"; -import { resolveOwnerMembership } from "../services/membership-tiers.js"; +import { resolveOwnerMembership, tierLabel } from "../services/membership-tiers.js"; import { inferDiagnosticAgentType } from "../lib/diagnostic-agent-type-inference.js"; import { isValidAdcpVersionShape } from "../services/adcp-taxonomy.js"; import { buildAaoVerificationBlock } from "../services/aao-verification-enrichment.js"; @@ -755,7 +755,10 @@ registry.registerPath({ "**Response shape is auth-aware.** Anonymous callers see only `public` agents. " + "Authenticated callers on an AAO membership tier with API access also see `members_only` agents. " + "Profile owners (callers whose org owns the queried domain) additionally see `private` agents. " + - "This is the primary mechanism by which AAO membership unlocks deeper registry visibility.", + "This is the primary mechanism by which AAO membership unlocks deeper registry visibility.\n\n" + + "**Member tier visibility.** When the profile owner has set their member card to public " + + "(`is_public=true`), the `member` object additionally carries `membership_tier` (raw enum) and " + + "`membership_tier_label` (e.g. `Professional`, `Leader`). For private profiles these fields are absent.", tags: ["Authorization Lookups"], request: { query: z.object({ @@ -6042,8 +6045,30 @@ export function createRegistryApiRouter(config: RegistryApiConfig): Router { const federatedIndex = crawler.getFederatedIndex(); const profile = await memberDb.getProfileByDomain(domain); + + // Membership tier is surfaced on the public response only when the + // profile owner has opted their member card into public visibility + // (`is_public=true`). Tier reflects billing state, so we don't leak it + // for private profiles even though slug/display_name are exposed for + // domain-keyed lookup. Profile owner controls visibility via the member + // card; we follow it here rather than introducing a second toggle. + let memberTier: string | null = null; + if (profile?.is_public && profile.workos_organization_id) { + const profileOrg = await orgDb.getOrganization(profile.workos_organization_id); + memberTier = resolveMembershipTier(profileOrg); + } + const member = profile - ? { slug: profile.slug, display_name: profile.display_name } + ? { + slug: profile.slug, + display_name: profile.display_name, + ...(memberTier + ? { + membership_tier: memberTier, + membership_tier_label: tierLabel(memberTier), + } + : {}), + } : null; const callerOrgId = await resolveCallerOrgId(req); diff --git a/server/src/schemas/registry.ts b/server/src/schemas/registry.ts index e243a98683..e76eea4724 100644 --- a/server/src/schemas/registry.ts +++ b/server/src/schemas/registry.ts @@ -182,6 +182,14 @@ export const PropertySummarySchema = z const MemberRefSchema = z.object({ slug: z.string().optional(), display_name: z.string().optional(), + membership_tier: z.string().optional().openapi({ + description: + "Raw AAO membership tier enum (e.g. `individual_professional`, `company_leader`). Present only when the profile owner has set their member card to public (`is_public=true`) AND the org has a resolvable tier. Absent for private profiles and for orgs without an active tier-bearing subscription.", + }), + membership_tier_label: z.string().optional().openapi({ + description: + "Human-readable label for `membership_tier` (e.g. `Professional`, `Leader`). Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`.", + }), }); export const ResolvedBrandSchema = z diff --git a/server/tests/unit/operator-publisher-lookup.test.ts b/server/tests/unit/operator-publisher-lookup.test.ts index 5b51add719..181ff6543f 100644 --- a/server/tests/unit/operator-publisher-lookup.test.ts +++ b/server/tests/unit/operator-publisher-lookup.test.ts @@ -37,6 +37,38 @@ describe('OperatorLookupResult schema', () => { } }); + it('validates a public profile with membership_tier fields', () => { + const data = { + domain: 'scope3.com', + member: { + slug: 'scope3', + display_name: 'Scope3', + membership_tier: 'company_leader', + membership_tier_label: 'Leader', + }, + agents: [], + }; + const result = OperatorLookupResultSchema.safeParse(data); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.member?.membership_tier).toBe('company_leader'); + expect(result.data.member?.membership_tier_label).toBe('Leader'); + } + }); + + it('validates a private profile without membership_tier fields', () => { + const data = { + domain: 'private.example', + member: { slug: 'private', display_name: 'Private Co' }, + agents: [], + }; + const result = OperatorLookupResultSchema.safeParse(data); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.member?.membership_tier).toBeUndefined(); + } + }); + it('validates an unfound operator', () => { const data = { domain: 'unknown.com', diff --git a/static/openapi/registry.yaml b/static/openapi/registry.yaml index 548dbc167e..d5ef730b8a 100644 --- a/static/openapi/registry.yaml +++ b/static/openapi/registry.yaml @@ -387,6 +387,12 @@ components: type: string display_name: type: string + membership_tier: + type: string + description: Raw AAO membership tier enum (e.g. `individual_professional`, `company_leader`). Present only when the profile owner has set their member card to public (`is_public=true`) AND the org has a resolvable tier. Absent for private profiles and for orgs without an active tier-bearing subscription. + membership_tier_label: + type: string + description: Human-readable label for `membership_tier` (e.g. `Professional`, `Leader`). Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`. description: AAO member that owns this agent record. The registry contains only agents that members have explicitly enrolled on their member profile. health: $ref: "#/components/schemas/AgentHealth" @@ -652,6 +658,12 @@ components: type: string display_name: type: string + membership_tier: + type: string + description: Raw AAO membership tier enum (e.g. `individual_professional`, `company_leader`). Present only when the profile owner has set their member card to public (`is_public=true`) AND the org has a resolvable tier. Absent for private profiles and for orgs without an active tier-bearing subscription. + membership_tier_label: + type: string + description: Human-readable label for `membership_tier` (e.g. `Professional`, `Leader`). Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`. agent_count: type: integer last_validated: @@ -682,6 +694,12 @@ components: type: string display_name: type: string + membership_tier: + type: string + description: Raw AAO membership tier enum (e.g. `individual_professional`, `company_leader`). Present only when the profile owner has set their member card to public (`is_public=true`) AND the org has a resolvable tier. Absent for private profiles and for orgs without an active tier-bearing subscription. + membership_tier_label: + type: string + description: Human-readable label for `membership_tier` (e.g. `Professional`, `Leader`). Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`. required: - url sales_agents_claiming: @@ -698,6 +716,12 @@ components: type: string display_name: type: string + membership_tier: + type: string + description: Raw AAO membership tier enum (e.g. `individual_professional`, `company_leader`). Present only when the profile owner has set their member card to public (`is_public=true`) AND the org has a resolvable tier. Absent for private profiles and for orgs without an active tier-bearing subscription. + membership_tier_label: + type: string + description: Human-readable label for `membership_tier` (e.g. `Professional`, `Leader`). Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`. required: - url required: @@ -719,6 +743,12 @@ components: type: string display_name: type: string + membership_tier: + type: string + description: Raw AAO membership tier enum (e.g. `individual_professional`, `company_leader`). Present only when the profile owner has set their member card to public (`is_public=true`) AND the org has a resolvable tier. Absent for private profiles and for orgs without an active tier-bearing subscription. + membership_tier_label: + type: string + description: Human-readable label for `membership_tier` (e.g. `Professional`, `Leader`). Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`. agents: type: array items: @@ -781,6 +811,12 @@ components: type: string display_name: type: string + membership_tier: + type: string + description: Raw AAO membership tier enum (e.g. `individual_professional`, `company_leader`). Present only when the profile owner has set their member card to public (`is_public=true`) AND the org has a resolvable tier. Absent for private profiles and for orgs without an active tier-bearing subscription. + membership_tier_label: + type: string + description: Human-readable label for `membership_tier` (e.g. `Professional`, `Leader`). Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`. adagents_valid: type: - boolean @@ -3360,6 +3396,8 @@ paths: Given a domain, returns the agents this entity operates and which publishers trust them. **Response shape is auth-aware.** Anonymous callers see only `public` agents. Authenticated callers on an AAO membership tier with API access also see `members_only` agents. Profile owners (callers whose org owns the queried domain) additionally see `private` agents. This is the primary mechanism by which AAO membership unlocks deeper registry visibility. + + **Member tier visibility.** When the profile owner has set their member card to public (`is_public=true`), the `member` object additionally carries `membership_tier` (raw enum) and `membership_tier_label` (e.g. `Professional`, `Leader`). For private profiles these fields are absent. tags: - Authorization Lookups parameters: From 9c8b85c8e0e81b342da39a1396e1118d04e18ecf Mon Sep 17 00:00:00 2001 From: Emma Mulitz Date: Wed, 13 May 2026 22:07:50 -0400 Subject: [PATCH 2/3] fix(operator-lookup): add is_founding_member and correct Partner label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Public operator lookup now exposes is_founding_member alongside the membership tier when the profile is public. Founding Member is orthogonal to tier — founding orgs like Scope3 display both (Partner + Founding Member) on their member card, so a single tier field doesn't capture the intent. Also fixes the company_icl label in services/membership-tiers.ts from "Member" to "Partner" so it matches the AAO pricing page and the dashboard's local tier map (which already used "Partner"). The label was internal-only previously; surfacing it on the operator endpoint forced the naming inconsistency to surface. Schema, OpenAPI spec, and unit tests updated. Private profiles still return only { slug, display_name } — no shape change for existing consumers. --- .../operator-member-tier-public-card.md | 32 ++++++++---- server/src/routes/registry-api.ts | 25 ++++++---- server/src/schemas/registry.ts | 6 ++- server/src/services/membership-tiers.ts | 2 +- server/tests/unit/membership-tiers.test.ts | 2 +- .../unit/operator-publisher-lookup.test.ts | 49 +++++++++++++++++-- static/openapi/registry.yaml | 32 +++++++++--- 7 files changed, 117 insertions(+), 31 deletions(-) diff --git a/.changeset/operator-member-tier-public-card.md b/.changeset/operator-member-tier-public-card.md index e3b66d20f6..2e98e24871 100644 --- a/.changeset/operator-member-tier-public-card.md +++ b/.changeset/operator-member-tier-public-card.md @@ -1,12 +1,26 @@ --- --- -`/api/registry/operator?domain=X` now surfaces the queried member's AAO -membership tier when the profile owner has opted their member card into public -visibility (`is_public=true`). The `member` object grows two optional fields: -`membership_tier` (raw enum, e.g. `company_leader`) and `membership_tier_label` -(human-readable, e.g. `Leader`). Private profiles still return only -`{ slug, display_name }` — tier reflects billing state and we follow the -profile-card visibility toggle rather than introducing a second one. The -fields are absent (not `null`) for private profiles and for orgs without a -resolvable tier so existing consumers see no shape change. +`/api/registry/operator?domain=X` now surfaces the queried member's public +level info when the profile owner has opted their member card into public +visibility (`is_public=true`). The `member` object grows three optional +fields: + +- `is_founding_member` (boolean) — present whenever the profile is public + (true or false; absent for private profiles) +- `membership_tier` (raw enum, e.g. `company_icl`, `company_leader`) — + present only when the org also has a resolvable tier +- `membership_tier_label` (human-readable, e.g. `Partner`, `Leader`) — + matches the AAO pricing page; presence mirrors `membership_tier` + +Private profiles still return only `{ slug, display_name }`. Tier and +founding-member status reflect billing/cohort state and follow the existing +profile-card visibility toggle rather than introducing a second one. Fields +are absent (not `null`) when not applicable, so existing consumers see no +shape change. + +Also fixes the `company_icl` label in `tierLabel()` from `Member` to +`Partner` so it matches the public pricing page and the dashboard (which +already used `Partner`). Founding Member is orthogonal to tier — founding +orgs typically display both badges (e.g. Scope3 shows `Partner` + +`Founding Member`). diff --git a/server/src/routes/registry-api.ts b/server/src/routes/registry-api.ts index 095300ce72..c5c84cb1ea 100644 --- a/server/src/routes/registry-api.ts +++ b/server/src/routes/registry-api.ts @@ -756,9 +756,11 @@ registry.registerPath({ "Authenticated callers on an AAO membership tier with API access also see `members_only` agents. " + "Profile owners (callers whose org owns the queried domain) additionally see `private` agents. " + "This is the primary mechanism by which AAO membership unlocks deeper registry visibility.\n\n" + - "**Member tier visibility.** When the profile owner has set their member card to public " + - "(`is_public=true`), the `member` object additionally carries `membership_tier` (raw enum) and " + - "`membership_tier_label` (e.g. `Professional`, `Leader`). For private profiles these fields are absent.", + "**Member level visibility.** When the profile owner has set their member card to public " + + "(`is_public=true`), the `member` object additionally carries `is_founding_member` (boolean) " + + "plus `membership_tier` (raw enum) and `membership_tier_label` (e.g. `Professional`, `Partner`, " + + "`Leader`) when the org has a resolvable tier. Founding Member is orthogonal to tier — founding " + + "orgs typically display both. For private profiles these fields are absent.", tags: ["Authorization Lookups"], request: { query: z.object({ @@ -6046,12 +6048,14 @@ export function createRegistryApiRouter(config: RegistryApiConfig): Router { const profile = await memberDb.getProfileByDomain(domain); - // Membership tier is surfaced on the public response only when the - // profile owner has opted their member card into public visibility - // (`is_public=true`). Tier reflects billing state, so we don't leak it - // for private profiles even though slug/display_name are exposed for - // domain-keyed lookup. Profile owner controls visibility via the member - // card; we follow it here rather than introducing a second toggle. + // Membership tier and Founding Member status are surfaced on the + // public response only when the profile owner has opted their member + // card into public visibility (`is_public=true`). Tier reflects billing + // state, so we don't leak it for private profiles even though + // slug/display_name are exposed for domain-keyed lookup. Profile owner + // controls visibility via the member card; we follow it here rather + // than introducing a second toggle. Founding Member is orthogonal to + // tier — founding orgs typically display both badges. let memberTier: string | null = null; if (profile?.is_public && profile.workos_organization_id) { const profileOrg = await orgDb.getOrganization(profile.workos_organization_id); @@ -6062,6 +6066,9 @@ export function createRegistryApiRouter(config: RegistryApiConfig): Router { ? { slug: profile.slug, display_name: profile.display_name, + ...(profile.is_public + ? { is_founding_member: profile.is_founding_member === true } + : {}), ...(memberTier ? { membership_tier: memberTier, diff --git a/server/src/schemas/registry.ts b/server/src/schemas/registry.ts index e76eea4724..f4eeea2f0d 100644 --- a/server/src/schemas/registry.ts +++ b/server/src/schemas/registry.ts @@ -188,7 +188,11 @@ const MemberRefSchema = z.object({ }), membership_tier_label: z.string().optional().openapi({ description: - "Human-readable label for `membership_tier` (e.g. `Professional`, `Leader`). Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`.", + "Human-readable label for `membership_tier` (e.g. `Professional`, `Partner`, `Leader`). Matches the AAO pricing page. Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`.", + }), + is_founding_member: z.boolean().optional().openapi({ + description: + "True when the profile owner carries the Founding Member badge (joined before the founding-cohort cutoff). Surfaced when the profile owner has set their member card to public (`is_public=true`). Absent for private profiles. Founding Member is orthogonal to tier — founding orgs typically display both (e.g. Scope3 shows `Partner` + `Founding Member`).", }), }); diff --git a/server/src/services/membership-tiers.ts b/server/src/services/membership-tiers.ts index 3c4595deee..eace07faec 100644 --- a/server/src/services/membership-tiers.ts +++ b/server/src/services/membership-tiers.ts @@ -55,7 +55,7 @@ const TIER_LABELS: Record = { explorer: 'Explorer', individual_professional: 'Professional', company_standard: 'Builder', - company_icl: 'Member', + company_icl: 'Partner', company_leader: 'Leader', }; diff --git a/server/tests/unit/membership-tiers.test.ts b/server/tests/unit/membership-tiers.test.ts index e31b2202e7..218d633048 100644 --- a/server/tests/unit/membership-tiers.test.ts +++ b/server/tests/unit/membership-tiers.test.ts @@ -72,7 +72,7 @@ describe('membership-tiers', () => { expect(tierLabel('explorer')).toBe('Explorer'); expect(tierLabel('individual_professional')).toBe('Professional'); expect(tierLabel('company_standard')).toBe('Builder'); - expect(tierLabel('company_icl')).toBe('Member'); + expect(tierLabel('company_icl')).toBe('Partner'); expect(tierLabel('company_leader')).toBe('Leader'); }); diff --git a/server/tests/unit/operator-publisher-lookup.test.ts b/server/tests/unit/operator-publisher-lookup.test.ts index 181ff6543f..b6e0921402 100644 --- a/server/tests/unit/operator-publisher-lookup.test.ts +++ b/server/tests/unit/operator-publisher-lookup.test.ts @@ -37,12 +37,34 @@ describe('OperatorLookupResult schema', () => { } }); - it('validates a public profile with membership_tier fields', () => { + it('validates a public founding-member profile with tier (Scope3 shape)', () => { const data = { domain: 'scope3.com', member: { slug: 'scope3', display_name: 'Scope3', + is_founding_member: true, + membership_tier: 'company_icl', + membership_tier_label: 'Partner', + }, + agents: [], + }; + const result = OperatorLookupResultSchema.safeParse(data); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.member?.is_founding_member).toBe(true); + expect(result.data.member?.membership_tier).toBe('company_icl'); + expect(result.data.member?.membership_tier_label).toBe('Partner'); + } + }); + + it('validates a public non-founding profile with tier', () => { + const data = { + domain: 'example.com', + member: { + slug: 'example', + display_name: 'Example Co', + is_founding_member: false, membership_tier: 'company_leader', membership_tier_label: 'Leader', }, @@ -51,12 +73,31 @@ describe('OperatorLookupResult schema', () => { const result = OperatorLookupResultSchema.safeParse(data); expect(result.success).toBe(true); if (result.success) { - expect(result.data.member?.membership_tier).toBe('company_leader'); + expect(result.data.member?.is_founding_member).toBe(false); expect(result.data.member?.membership_tier_label).toBe('Leader'); } }); - it('validates a private profile without membership_tier fields', () => { + it('validates a public profile without a resolvable tier (founding flag still present)', () => { + const data = { + domain: 'newco.example', + member: { + slug: 'newco', + display_name: 'NewCo', + is_founding_member: false, + }, + agents: [], + }; + const result = OperatorLookupResultSchema.safeParse(data); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data.member?.is_founding_member).toBe(false); + expect(result.data.member?.membership_tier).toBeUndefined(); + expect(result.data.member?.membership_tier_label).toBeUndefined(); + } + }); + + it('validates a private profile with no tier or founding fields', () => { const data = { domain: 'private.example', member: { slug: 'private', display_name: 'Private Co' }, @@ -65,7 +106,9 @@ describe('OperatorLookupResult schema', () => { const result = OperatorLookupResultSchema.safeParse(data); expect(result.success).toBe(true); if (result.success) { + expect(result.data.member?.is_founding_member).toBeUndefined(); expect(result.data.member?.membership_tier).toBeUndefined(); + expect(result.data.member?.membership_tier_label).toBeUndefined(); } }); diff --git a/static/openapi/registry.yaml b/static/openapi/registry.yaml index d5ef730b8a..88e28d2af6 100644 --- a/static/openapi/registry.yaml +++ b/static/openapi/registry.yaml @@ -392,7 +392,10 @@ components: description: Raw AAO membership tier enum (e.g. `individual_professional`, `company_leader`). Present only when the profile owner has set their member card to public (`is_public=true`) AND the org has a resolvable tier. Absent for private profiles and for orgs without an active tier-bearing subscription. membership_tier_label: type: string - description: Human-readable label for `membership_tier` (e.g. `Professional`, `Leader`). Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`. + description: Human-readable label for `membership_tier` (e.g. `Professional`, `Partner`, `Leader`). Matches the AAO pricing page. Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`. + is_founding_member: + type: boolean + description: True when the profile owner carries the Founding Member badge (joined before the founding-cohort cutoff). Surfaced when the profile owner has set their member card to public (`is_public=true`). Absent for private profiles. Founding Member is orthogonal to tier — founding orgs typically display both (e.g. Scope3 shows `Partner` + `Founding Member`). description: AAO member that owns this agent record. The registry contains only agents that members have explicitly enrolled on their member profile. health: $ref: "#/components/schemas/AgentHealth" @@ -663,7 +666,10 @@ components: description: Raw AAO membership tier enum (e.g. `individual_professional`, `company_leader`). Present only when the profile owner has set their member card to public (`is_public=true`) AND the org has a resolvable tier. Absent for private profiles and for orgs without an active tier-bearing subscription. membership_tier_label: type: string - description: Human-readable label for `membership_tier` (e.g. `Professional`, `Leader`). Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`. + description: Human-readable label for `membership_tier` (e.g. `Professional`, `Partner`, `Leader`). Matches the AAO pricing page. Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`. + is_founding_member: + type: boolean + description: True when the profile owner carries the Founding Member badge (joined before the founding-cohort cutoff). Surfaced when the profile owner has set their member card to public (`is_public=true`). Absent for private profiles. Founding Member is orthogonal to tier — founding orgs typically display both (e.g. Scope3 shows `Partner` + `Founding Member`). agent_count: type: integer last_validated: @@ -699,7 +705,10 @@ components: description: Raw AAO membership tier enum (e.g. `individual_professional`, `company_leader`). Present only when the profile owner has set their member card to public (`is_public=true`) AND the org has a resolvable tier. Absent for private profiles and for orgs without an active tier-bearing subscription. membership_tier_label: type: string - description: Human-readable label for `membership_tier` (e.g. `Professional`, `Leader`). Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`. + description: Human-readable label for `membership_tier` (e.g. `Professional`, `Partner`, `Leader`). Matches the AAO pricing page. Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`. + is_founding_member: + type: boolean + description: True when the profile owner carries the Founding Member badge (joined before the founding-cohort cutoff). Surfaced when the profile owner has set their member card to public (`is_public=true`). Absent for private profiles. Founding Member is orthogonal to tier — founding orgs typically display both (e.g. Scope3 shows `Partner` + `Founding Member`). required: - url sales_agents_claiming: @@ -721,7 +730,10 @@ components: description: Raw AAO membership tier enum (e.g. `individual_professional`, `company_leader`). Present only when the profile owner has set their member card to public (`is_public=true`) AND the org has a resolvable tier. Absent for private profiles and for orgs without an active tier-bearing subscription. membership_tier_label: type: string - description: Human-readable label for `membership_tier` (e.g. `Professional`, `Leader`). Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`. + description: Human-readable label for `membership_tier` (e.g. `Professional`, `Partner`, `Leader`). Matches the AAO pricing page. Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`. + is_founding_member: + type: boolean + description: True when the profile owner carries the Founding Member badge (joined before the founding-cohort cutoff). Surfaced when the profile owner has set their member card to public (`is_public=true`). Absent for private profiles. Founding Member is orthogonal to tier — founding orgs typically display both (e.g. Scope3 shows `Partner` + `Founding Member`). required: - url required: @@ -748,7 +760,10 @@ components: description: Raw AAO membership tier enum (e.g. `individual_professional`, `company_leader`). Present only when the profile owner has set their member card to public (`is_public=true`) AND the org has a resolvable tier. Absent for private profiles and for orgs without an active tier-bearing subscription. membership_tier_label: type: string - description: Human-readable label for `membership_tier` (e.g. `Professional`, `Leader`). Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`. + description: Human-readable label for `membership_tier` (e.g. `Professional`, `Partner`, `Leader`). Matches the AAO pricing page. Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`. + is_founding_member: + type: boolean + description: True when the profile owner carries the Founding Member badge (joined before the founding-cohort cutoff). Surfaced when the profile owner has set their member card to public (`is_public=true`). Absent for private profiles. Founding Member is orthogonal to tier — founding orgs typically display both (e.g. Scope3 shows `Partner` + `Founding Member`). agents: type: array items: @@ -816,7 +831,10 @@ components: description: Raw AAO membership tier enum (e.g. `individual_professional`, `company_leader`). Present only when the profile owner has set their member card to public (`is_public=true`) AND the org has a resolvable tier. Absent for private profiles and for orgs without an active tier-bearing subscription. membership_tier_label: type: string - description: Human-readable label for `membership_tier` (e.g. `Professional`, `Leader`). Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`. + description: Human-readable label for `membership_tier` (e.g. `Professional`, `Partner`, `Leader`). Matches the AAO pricing page. Use this for UI display; the raw enum is for programmatic gating. Presence rules match `membership_tier`. + is_founding_member: + type: boolean + description: True when the profile owner carries the Founding Member badge (joined before the founding-cohort cutoff). Surfaced when the profile owner has set their member card to public (`is_public=true`). Absent for private profiles. Founding Member is orthogonal to tier — founding orgs typically display both (e.g. Scope3 shows `Partner` + `Founding Member`). adagents_valid: type: - boolean @@ -3397,7 +3415,7 @@ paths: **Response shape is auth-aware.** Anonymous callers see only `public` agents. Authenticated callers on an AAO membership tier with API access also see `members_only` agents. Profile owners (callers whose org owns the queried domain) additionally see `private` agents. This is the primary mechanism by which AAO membership unlocks deeper registry visibility. - **Member tier visibility.** When the profile owner has set their member card to public (`is_public=true`), the `member` object additionally carries `membership_tier` (raw enum) and `membership_tier_label` (e.g. `Professional`, `Leader`). For private profiles these fields are absent. + **Member level visibility.** When the profile owner has set their member card to public (`is_public=true`), the `member` object additionally carries `is_founding_member` (boolean) plus `membership_tier` (raw enum) and `membership_tier_label` (e.g. `Professional`, `Partner`, `Leader`) when the org has a resolvable tier. Founding Member is orthogonal to tier — founding orgs typically display both. For private profiles these fields are absent. tags: - Authorization Lookups parameters: From 995f9cbc8b3c468acc5d3eaee704597579cecc52 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Thu, 14 May 2026 10:10:26 -0400 Subject: [PATCH 3/3] chore(openapi): regenerate registry.yaml after rebase Re-regen captures Emma's three new member.* fields plus a small post-#4515 drift sweep that landed today (verification_mode/verified query params on agent listing; 400 response shape). Co-Authored-By: Claude Opus 4.7 (1M context) --- static/openapi/registry.yaml | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/static/openapi/registry.yaml b/static/openapi/registry.yaml index 88e28d2af6..36fefe73f6 100644 --- a/static/openapi/registry.yaml +++ b/static/openapi/registry.yaml @@ -3248,6 +3248,27 @@ paths: description: "Measurement-vendor filter: case-insensitive substring match against `measurement.metrics[].metric_id`. v1 scope: metric_id only (description/standard search is a follow-up). Max 64 chars; SQL wildcard characters are escaped. Implies `type=measurement`." name: q in: query + - schema: + type: array + items: + type: string + enum: + - spec + - live + description: "Filter to agents whose active badge covers the given verification axis. Repeat the parameter for AND semantics: ?verification_mode=spec&verification_mode=live returns only agents verified on both axes." + required: false + description: "Filter to agents whose active badge covers the given verification axis. Repeat the parameter for AND semantics: ?verification_mode=spec&verification_mode=live returns only agents verified on both axes." + name: verification_mode + in: query + - schema: + type: string + enum: + - "true" + description: When true, filter to agents that hold any active verification badge. + required: false + description: When true, filter to agents that hold any active verification badge. + name: verified + in: query responses: "200": description: Agent list @@ -3270,7 +3291,16 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/Error" + type: object + properties: + error: + type: string + valid_values: + type: array + items: + type: string + required: + - error /api/registry/publishers: get: operationId: listPublishers