Skip to main content

Agent

The agents table in marketplace-fleapoai-service holds every agent's catalog listing. It is not a runtime — see Architecture.

Agent types​

Every agent is one of two types, fixed by type at creation:

TypeSells through the marketplace?Signup & billingPrimary CTA
integratedYes — Stripe checkout, pricing tiers, subscriptionsMarketplace account, optionally federated into the agent over OAuth"Choose a plan"
listedNoThe agent's own, entirely off-platform"Visit site" → externalUrl

A listed agent is a directory entry. We host the listing — icon, screenshots, video, capabilities, steps, trust signals — and link out. We do not take payment for it, issue it an OAuth client, or meter its usage.

Practical consequences for a listed agent:

  • Both checkout endpoints return 400. This is enforced in SubscriptionsService.assertTransactable(), not just hidden in the UI.
  • GET /agents/:id/pricing returns [] to public callers. Platform admins still see every tier, because the conversion path below requires adding one.
  • externalUrl is required (422 otherwise) and is the only CTA on the card and detail page.
  • Reviews, ratings, tags, waitlist and demo requests behave exactly as they do for integrated agents.
  • The detail page suppresses the productDomain link, so there is exactly one outbound destination.

Conversion​

Type changes are one-way:

listed → integrated is the upgrade path for a partner who starts as a link and later builds the real integration. It happens in place, so the row keeps its slug, reviews, view count and search ranking. It requires, and is validated against, both:

  1. at least one OAuth redirect URI, and
  2. at least one pricing tier.

Without (1) the agent could never mint an OAuth client; without (2) it would flip to integrated and render an empty pricing card. Each missing prerequisite returns its own 422.

integrated → listed is refused (422). It would orphan live OAuth clients and active subscriptions. To retire an integration, set status to inactive instead.

Schema​

ColumnTypeNotes
idUUIDPrimary key
nametextHuman-readable name
slugtext, uniqueURL slug; immutable after publish
descriptiontextMarketing description (detail page hero)
longDescriptiontext, nullableMulti-paragraph body copy
taglinetextOne-sentence pitch
iconUrltext, nullableCDN-hosted icon
photoUrlstext[]Screenshots gallery
typeenum, NOT NULLintegrated | listed, default integrated, CHECK check_agents_type. See Agent types
externalUrltext, nullableOutbound CTA target. Required when type = listed
pricingUrltext, nullableOptional link to the agent's own pricing page
statusenumdraft | active | coming_soon | inactive
featuredboolHomepage placement
isNewboolRenders the "New" badge
productDomaintext, nullableAgent's frontend URL (used to validate returnUrl on checkout)
docsUrltext, nullableOutbound link to agent's docs
changelogUrltext, nullableOutbound link
oauthClientIdtext, uniqueReference into better-auth oauthClient row
redirectUristext[]OAuth allow-list; mirrored into oauthClient
createdByUUIDauth.user.id of admin who created
publishedAttimestamptz, nullableSet when status flips to active
createdAt, updatedAttimestamptz

Lifecycle​

status controls visibility and OAuth client enablement. See Publishing & approval.

DTO​

type AgentDto = {
id: string;
slug: string;
name: string;
tagline: string;
description: string;
longDescription: string | null;
iconUrl: string | null;
photoUrls: string[];
status: "draft" | "active" | "coming_soon" | "inactive";
featured: boolean;
isNew: boolean;
publishedAt: string | null;
type: "integrated" | "listed";
externalUrl: string | null; // non-null when type === "listed"
pricingUrl: string | null;
productDomain: string | null;
docsUrl: string | null;
changelogUrl: string | null;
oauthClientId: string;
redirectUris: string[];

capabilities: CapabilityDto[];
steps: StepDto[];
changelog: ChangelogEntryDto[];
trustSignals: string[];
tags: TagDto[];

rating: number | null; // 1-decimal avg, null if reviewCount === 0
reviewCount: number;
installs: number;

createdBy: string;
createdAt: string;
updatedAt: string;
};

The frontend converts this to a leaner Agent shape via toDisplayAgent() in src/data/agents.ts.

Endpoints​

VerbPathGuard
GET/agentspublic, paginated
GET/agents/:idpublic
GET/agents/by-slug/:slugpublic
POST/agentsplatform_admin
POST/agents/:idplatform_admin
DELETE/agents/:idplatform_admin
POST/agents/:id/demo-requestbearer

See API Reference for full schemas.

Identity bridge​

When you POST /agents, the service:

  1. Inserts the agents row.
  2. If redirectUris is non-empty, creates a better-auth oauthClient row (type public, tokenEndpointAuthMethod: 'none', PKCE-S256 enforced).
  3. Mirrors redirectUris from the agent row to the client row.
  4. Returns the new oauthClientId.

On POST /agents/:id with a changed redirectUris[], the mirror is updated. An agent created without redirect URIs has oauthClientId: null and gains a client the first time URIs are supplied.

note

Listed agents never reach step 2 — they have no OAuth client and no identity bridge. Note also that oauthClientId being null does not mean an agent is listed: an integrated agent that simply has not finished its OAuth setup also has null here, and may already be selling. Always read type.

This is the only linkage between the catalog row and the OIDC client. There is no agent-side "key" issued by the marketplace beyond this.