Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .changeset/operator-member-tier-public-card.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
---

`/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`).
38 changes: 35 additions & 3 deletions server/src/routes/registry-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -755,7 +755,12 @@ 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 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({
Expand Down Expand Up @@ -6042,8 +6047,35 @@ export function createRegistryApiRouter(config: RegistryApiConfig): Router {
const federatedIndex = crawler.getFederatedIndex();

const profile = await memberDb.getProfileByDomain(domain);

// 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);
memberTier = resolveMembershipTier(profileOrg);
}

const member = profile
? { slug: profile.slug, display_name: profile.display_name }
? {
slug: profile.slug,
display_name: profile.display_name,
...(profile.is_public
? { is_founding_member: profile.is_founding_member === true }
: {}),
...(memberTier
? {
membership_tier: memberTier,
membership_tier_label: tierLabel(memberTier),
}
: {}),
}
: null;

const callerOrgId = await resolveCallerOrgId(req);
Expand Down
12 changes: 12 additions & 0 deletions server/src/schemas/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,18 @@ 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`, `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`).",
}),
});

export const ResolvedBrandSchema = z
Expand Down
2 changes: 1 addition & 1 deletion server/src/services/membership-tiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const TIER_LABELS: Record<string, string> = {
explorer: 'Explorer',
individual_professional: 'Professional',
company_standard: 'Builder',
company_icl: 'Member',
company_icl: 'Partner',
company_leader: 'Leader',
};

Expand Down
2 changes: 1 addition & 1 deletion server/tests/unit/membership-tiers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});

Expand Down
75 changes: 75 additions & 0 deletions server/tests/unit/operator-publisher-lookup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,81 @@ describe('OperatorLookupResult schema', () => {
}
});

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',
},
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_label).toBe('Leader');
}
});

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' },
agents: [],
};
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();
}
});

it('validates an unfound operator', () => {
const data = {
domain: 'unknown.com',
Expand Down
Loading
Loading