+
+
Usage & Billing
+
+
+
+ {/* Error state */}
+ {state.loadState === 'error' && (
+
+ {state.errorMessage ?? 'Failed to load usage data.'}
+
+ )}
+
+ {/* Loading skeleton */}
+ {isLoading && (
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+ )}
+
+ {/* Data */}
+ {state.loadState === 'success' && state.data !== null && (
+ <>
+
+
+ Showing usage for {state.data.date}
+
+
+
+
+
+
+
+
+
+
+ {state.data.subscriptionStatus === 'free' && (
+
+
+ You are on the Free Tier — limited to 10 agents and 1,000 API calls/day.
+
+
+ Upgrade to Pro for unlimited agents and API calls.
+
+
+ )}
+
+ {state.data.currentPeriodEnd !== null && (
+
+ Current period ends:{' '}
+ {new Date(state.data.currentPeriodEnd).toLocaleDateString()}
+
+ )}
+ >
+ )}
+
+ );
+}
diff --git a/dashboard/src/components/layout/AppShell.tsx b/dashboard/src/components/layout/AppShell.tsx
index b0db26c..30270da 100644
--- a/dashboard/src/components/layout/AppShell.tsx
+++ b/dashboard/src/components/layout/AppShell.tsx
@@ -12,6 +12,7 @@ const NAV_ITEMS: NavItem[] = [
{ to: '/dashboard/agents', label: 'Agents' },
{ to: '/dashboard/audit', label: 'Audit Log' },
{ to: '/dashboard/health', label: 'Health' },
+ { to: '/dashboard/usage', label: 'Usage' },
];
/**
diff --git a/openspec/changes/phase-4-developer-growth/tasks.md b/openspec/changes/phase-4-developer-growth/tasks.md
index d0ae63e..bf18909 100644
--- a/openspec/changes/phase-4-developer-growth/tasks.md
+++ b/openspec/changes/phase-4-developer-growth/tasks.md
@@ -93,19 +93,19 @@
## 10. WS6: Billing & Usage Metering
-- [ ] 10.1 Create migration `007_add_billing.sql` — `tenant_subscriptions` table (tenant_id, status, stripe_customer_id, stripe_subscription_id, current_period_end) and `usage_events` table (tenant_id, date, metric_type, count)
-- [ ] 10.2 Install `stripe` npm package — add to package.json
-- [ ] 10.3 Create `UsageMeteringMiddleware` — increments in-memory per-tenant counters on every authenticated request; flushes to `usage_events` every 60s
-- [ ] 10.4 Create `UsageService` with `getDailyUsage(tenantId, date)` and `getActivAgentCount(tenantId)` methods
-- [ ] 10.5 Create `FreeTierEnforcementMiddleware` — checks usage cache (Redis, 60s TTL) before agent creation and API calls; rejects with HTTP 429 when limit exceeded; skips when `BILLING_ENABLED=false`
-- [ ] 10.6 Add `agentidp_billing_limit_rejections_total` Prometheus counter (labels: `tenant_id`, `limit_type`)
-- [ ] 10.7 Create `BillingService` with `createCheckoutSession(tenantId)`, `handleWebhookEvent(event)`, `getSubscriptionStatus(tenantId)` methods
-- [ ] 10.8 Create `POST /billing/checkout` endpoint — creates Stripe Checkout session, returns `checkoutUrl`
-- [ ] 10.9 Create `POST /billing/webhook` endpoint — verifies Stripe signature, processes subscription events, updates `tenant_subscriptions`
-- [ ] 10.10 Create `GET /billing/usage` endpoint (authenticated) — returns current period usage summary for tenant
-- [ ] 10.11 Add `BILLING_ENABLED` env var — disable enforcement and Stripe processing when false; document in `.env.example`
-- [ ] 10.12 Write unit tests for UsageService, BillingService, FreeTierEnforcementMiddleware — free tier block, paid tier pass-through, webhook processing
-- [ ] 10.13 Update web dashboard — add "Usage" tab to navigation with billing status panel and usage metrics from `GET /billing/usage`
+- [x] 10.1 Create migration `007_add_billing.sql` — `tenant_subscriptions` table (tenant_id, status, stripe_customer_id, stripe_subscription_id, current_period_end) and `usage_events` table (tenant_id, date, metric_type, count)
+- [x] 10.2 Install `stripe` npm package — add to package.json
+- [x] 10.3 Create `UsageMeteringMiddleware` — increments in-memory per-tenant counters on every authenticated request; flushes to `usage_events` every 60s
+- [x] 10.4 Create `UsageService` with `getDailyUsage(tenantId, date)` and `getActivAgentCount(tenantId)` methods
+- [x] 10.5 Create `FreeTierEnforcementMiddleware` — checks usage cache (Redis, 60s TTL) before agent creation and API calls; rejects with HTTP 429 when limit exceeded; skips when `BILLING_ENABLED=false`
+- [x] 10.6 Add `agentidp_billing_limit_rejections_total` Prometheus counter (labels: `tenant_id`, `limit_type`)
+- [x] 10.7 Create `BillingService` with `createCheckoutSession(tenantId)`, `handleWebhookEvent(event)`, `getSubscriptionStatus(tenantId)` methods
+- [x] 10.8 Create `POST /billing/checkout` endpoint — creates Stripe Checkout session, returns `checkoutUrl`
+- [x] 10.9 Create `POST /billing/webhook` endpoint — verifies Stripe signature, processes subscription events, updates `tenant_subscriptions`
+- [x] 10.10 Create `GET /billing/usage` endpoint (authenticated) — returns current period usage summary for tenant
+- [x] 10.11 Add `BILLING_ENABLED` env var — disable enforcement and Stripe processing when false; document in `.env.example`
+- [x] 10.12 Write unit tests for UsageService, BillingService, FreeTierEnforcementMiddleware — free tier block, paid tier pass-through, webhook processing
+- [x] 10.13 Update web dashboard — add "Usage" tab to navigation with billing status panel and usage metrics from `GET /billing/usage`
## 11. QA & Release
diff --git a/package-lock.json b/package-lock.json
index 9233fae..370eecc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,6 +30,7 @@
"prom-client": "^15.1.3",
"rate-limiter-flexible": "^5.0.5",
"redis": "^4.6.13",
+ "stripe": "^21.0.1",
"ulid": "^3.0.2",
"uuid": "^9.0.1",
"web-did-resolver": "^2.0.32"
@@ -1833,7 +1834,7 @@
"version": "20.19.37",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz",
"integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@@ -7619,6 +7620,23 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/stripe": {
+ "version": "21.0.1",
+ "resolved": "https://registry.npmjs.org/stripe/-/stripe-21.0.1.tgz",
+ "integrity": "sha512-ocv0j7dWttswDWV2XL/kb6+yiLpDXNXL3RQAOB5OB2kr49z0cEatdQc12+zP/j5nrXk6rAsT4N3y/NUvBbK7Pw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
"node_modules/superagent": {
"version": "8.1.2",
"resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz",
@@ -8044,7 +8062,7 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/unpipe": {
diff --git a/package.json b/package.json
index 283073b..9c7906b 100644
--- a/package.json
+++ b/package.json
@@ -38,6 +38,7 @@
"prom-client": "^15.1.3",
"rate-limiter-flexible": "^5.0.5",
"redis": "^4.6.13",
+ "stripe": "^21.0.1",
"ulid": "^3.0.2",
"uuid": "^9.0.1",
"web-did-resolver": "^2.0.32"
diff --git a/src/app.ts b/src/app.ts
index 35a7d35..a279826 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -8,6 +8,7 @@ import express, { Application } from 'express';
import helmet from 'helmet';
import cors from 'cors';
import morgan from 'morgan';
+import Stripe from 'stripe';
import { getPool } from './db/pool.js';
import { getRedisClient } from './cache/redis.js';
@@ -21,6 +22,8 @@ import { OrgRepository } from './repositories/OrgRepository.js';
import { AuditService } from './services/AuditService.js';
import { AgentService } from './services/AgentService.js';
import { MarketplaceService } from './services/MarketplaceService.js';
+import { BillingService } from './services/BillingService.js';
+import { UsageService } from './services/UsageService.js';
import { CredentialService } from './services/CredentialService.js';
import { OAuth2Service } from './services/OAuth2Service.js';
import { OrgService } from './services/OrgService.js';
@@ -35,6 +38,7 @@ import { createKafkaProducer } from './adapters/KafkaAdapter.js';
import { AgentController } from './controllers/AgentController.js';
import { MarketplaceController } from './controllers/MarketplaceController.js';
+import { BillingController } from './controllers/BillingController.js';
import { OIDCTrustPolicyController } from './controllers/OIDCTrustPolicyController.js';
import { OIDCTokenExchangeController } from './controllers/OIDCTokenExchangeController.js';
import { TokenController } from './controllers/TokenController.js';
@@ -49,6 +53,7 @@ import { ComplianceController } from './controllers/ComplianceController.js';
import { createAgentsRouter } from './routes/agents.js';
import { createMarketplaceRouter } from './routes/marketplace.js';
+import { createBillingRouter } from './routes/billing.js';
import { createOIDCTrustPoliciesRouter } from './routes/oidcTrustPolicies.js';
import { createOIDCTokenExchangeRouter } from './routes/oidcTokenExchange.js';
import { OIDCTrustPolicyService } from './services/OIDCTrustPolicyService.js';
@@ -69,6 +74,8 @@ import { createOpaMiddleware } from './middleware/opa.js';
import { metricsMiddleware } from './middleware/metrics.js';
import { createOrgContextMiddleware } from './middleware/orgContext.js';
import { authMiddleware } from './middleware/auth.js';
+import { createUsageMeteringMiddleware, startUsageMeteringFlush } from './middleware/usageMeteringMiddleware.js';
+import { createFreeTierEnforcementMiddleware } from './middleware/freeTierEnforcementMiddleware.js';
import { tlsEnforcementMiddleware } from './middleware/TLSEnforcementMiddleware.js';
import { createVaultClientFromEnv } from './vault/VaultClient.js';
import { getEncryptionService } from './services/EncryptionService.js';
@@ -232,6 +239,17 @@ export async function createApp(): Promise