Subscription
A subscription is the (user, agent, tier) triple that grants a user access to the agent's tier-gated features and meters their usage.
Schema (user_agent_subscriptions)
| Column | Type | Notes |
|---|---|---|
id | UUID | PK |
userId | UUID | FK → user |
agentId | UUID | FK → agents |
tierId | UUID | FK → agent_pricing_tiers |
status | enum | See below |
stripeCustomerId | text | Created on first paid checkout |
stripeSubscriptionId | text, unique nullable | NULL for free tiers |
currentPeriodEnd | timestamptz | When the current billing period ends |
cancelAtPeriodEnd | bool | If true, will cancel at period end |
canceledAt | timestamptz, nullable | Set on customer.subscription.deleted |
createdAt, updatedAt | timestamptz |
Partial unique index: (userId, agentId) WHERE status IN ('active', 'trialing'). This guarantees a user has at most one currently-billable row per agent.
Status values
| Status | Source | Meaning |
|---|---|---|
active | Stripe customer.subscription.created/updated OR free-tier checkout | Currently paying or on a free tier |
trialing | Stripe (during free trial window) | Inside trialDays |
past_due | Stripe invoice.payment_failed | Payment failed; grace period |
incomplete | Stripe customer.subscription.created (3DS pending) | Initial charge not yet succeeded |
incomplete_expired | Stripe | Initial charge failed terminally |
unpaid | Stripe (after retries exhausted) | All retries failed |
canceled | Stripe customer.subscription.deleted OR explicit user cancel | Terminated |
Agents should treat active and trialing as "user can use the agent." Everything else is "no access."
Lifecycle
Endpoints
| Verb | Path | Guard |
|---|---|---|
| POST | /agents/:agentId/pricing/:tierId/checkout | bearer |
| GET | /me/subscriptions | bearer |
| GET | /me/subscriptions/:agentId | bearer |
| POST | /me/subscriptions/:agentId/cancel | bearer |
DTO
type SubscriptionDto = {
id: string;
agentId: string;
tierId: string;
status: SubscriptionStatus;
stripeCustomerId: string | null;
stripeSubscriptionId: string | null;
currentPeriodEnd: string;
cancelAtPeriodEnd: boolean;
canceledAt: string | null;
tier: PricingTierDto; // embedded
createdAt: string;
updatedAt: string;
};
Cancel
POST /me/subscriptions/:agentId/cancel
- Free tier — immediate.
status→canceled. - Paid tier —
cancelAtPeriodEnd: true. The user keeps access untilcurrentPeriodEnd; Stripe sendscustomer.subscription.deletedthen.
Checkout outcomes
| Tier type | What happens | What the API returns |
|---|---|---|
| Free | Row inserted directly, status active | { status: 'active', subscriptionId, returnUrl } |
| Paid | Stripe Checkout Session created | { status: 'redirect', url } — browser follows it |
After payment, Stripe calls POST /webhooks/stripe with checkout.session.completed, which upserts the row.
Source of truth for tier
Always. Even if the agent has a marketplace:agentTiers hint in the ID token, the live subscription is authoritative. Call GET /me/subscriptions/:agentId whenever you need to read tier and you haven't read it in the last few seconds.