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:
| Type | Sells through the marketplace? | Signup & billing | Primary CTA |
|---|---|---|---|
integrated | Yes — Stripe checkout, pricing tiers, subscriptions | Marketplace account, optionally federated into the agent over OAuth | "Choose a plan" |
listed | No | The 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/pricingreturns[]to public callers. Platform admins still see every tier, because the conversion path below requires adding one.externalUrlis 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
productDomainlink, 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:
- at least one OAuth redirect URI, and
- 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
| Column | Type | Notes |
|---|---|---|
id | UUID | Primary key |
name | text | Human-readable name |
slug | text, unique | URL slug; immutable after publish |
description | text | Marketing description (detail page hero) |
longDescription | text, nullable | Multi-paragraph body copy |
tagline | text | One-sentence pitch |
iconUrl | text, nullable | CDN-hosted icon |
photoUrls | text[] | Screenshots gallery |
type | enum, NOT NULL | integrated | listed, default integrated, CHECK check_agents_type. See Agent types |
externalUrl | text, nullable | Outbound CTA target. Required when type = listed |
pricingUrl | text, nullable | Optional link to the agent's own pricing page |
status | enum | draft | active | coming_soon | inactive |
featured | bool | Homepage placement |
isNew | bool | Renders the "New" badge |
productDomain | text, nullable | Agent's frontend URL (used to validate returnUrl on checkout) |
docsUrl | text, nullable | Outbound link to agent's docs |
changelogUrl | text, nullable | Outbound link |
oauthClientId | text, unique | Reference into better-auth oauthClient row |
redirectUris | text[] | OAuth allow-list; mirrored into oauthClient |
createdBy | UUID | auth.user.id of admin who created |
publishedAt | timestamptz, nullable | Set when status flips to active |
createdAt, updatedAt | timestamptz |
Related tables
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
| Verb | Path | Guard |
|---|---|---|
| GET | /agents | public, paginated |
| GET | /agents/:id | public |
| GET | /agents/by-slug/:slug | public |
| POST | /agents | platform_admin |
| POST | /agents/:id | platform_admin |
| DELETE | /agents/:id | platform_admin |
| POST | /agents/:id/demo-request | bearer |
See API Reference for full schemas.
Identity bridge
When you POST /agents, the service:
- Inserts the
agentsrow. - If
redirectUrisis non-empty, creates a better-authoauthClientrow (typepublic,tokenEndpointAuthMethod: 'none', PKCE-S256 enforced). - Mirrors
redirectUrisfrom the agent row to the client row. - 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.
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.