# License Server Integration

**Version:** 1.0
**Last Updated:** 2026-05-12
**Status:** Production Ready

---

## Overview

Agencio Predict integrates with the License Server for:
- **Subscription Management** - Plan selection, checkout, billing portal
- **AI Credits** - Pay-per-use token billing with reserve/consume pattern
- **Deployment Validation** - License verification and anti-piracy protection
- **Customer Linking** - Maps Predict users to License Server customers

The License Server runs on AWS EKS at `https://licensing.agencio.cloud`.

---

## Architecture

```
┌─────────────────────────────────────────────────────────────────────────┐
│                        AGENCIO PREDICT (Vercel)                         │
│                                                                         │
│  ┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐     │
│  │ User Registration│    │  Billing Page   │    │  AI Usage       │     │
│  │ /onboarding     │    │  /settings/     │    │  LLM Calls      │     │
│  │                 │    │  ai-billing     │    │                 │     │
│  └────────┬────────┘    └────────┬────────┘    └────────┬────────┘     │
│           │                      │                      │               │
│           ▼                      ▼                      ▼               │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │              LICENSE SERVER INTEGRATION LAYER                    │   │
│  │                                                                  │   │
│  │  customer-service.ts  │  credit-client.ts  │  webhook-handler.ts│   │
│  │  (Customer CRUD)      │  (Credits API)     │  (Event Processing)│   │
│  └──────────────────────────────────┬──────────────────────────────┘   │
│                                     │                                   │
└─────────────────────────────────────┼───────────────────────────────────┘
                                      │ HTTPS
                                      ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                     LICENSE SERVER (AWS EKS)                            │
│                     https://licensing.agencio.cloud                     │
│                                                                         │
│  ┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐     │
│  │ Portal Routes   │    │ Credit Service  │    │ Stripe          │     │
│  │ /api/portal/*   │    │                 │    │ Integration     │     │
│  └─────────────────┘    └─────────────────┘    └─────────────────┘     │
│                                                                         │
│  ┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐     │
│  │ Deployment      │    │ Customer        │    │ Webhook         │     │
│  │ Validation      │    │ Management      │    │ Dispatch        │     │
│  └─────────────────┘    └─────────────────┘    └─────────────────┘     │
└─────────────────────────────────────────────────────────────────────────┘
```

---

## Environment Variables

### Required on Predict (Vercel)

| Variable | Description | Example |
|----------|-------------|---------|
| `LICENSE_SERVER_URL` | License Server base URL | `https://licensing.agencio.cloud` |
| `LICENSE_SERVER_API_KEY` | Server-to-server API key (matches `ADMIN_API_KEY` on LS) | `<32+ char secret>` |
| `LICENSE_SERVER_WEBHOOK_SECRET` | HMAC secret for webhook verification | `<32+ char secret>` |
| `DEPLOYMENT_KEY` | HMAC secret for deployment validation | `<32+ char secret>` |
| `DEPLOYMENT_ID` | Unique deployment identifier | `predict-prod-vercel` |

### Required on License Server (EKS)

| Variable | Description |
|----------|-------------|
| `ADMIN_API_KEY` | Server-to-server API key (share with Predict) |
| `STRIPE_SECRET_KEY` | Stripe API secret |
| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signature secret |
| `DATABASE_URL` | PostgreSQL connection string |
| `JWT_SECRET` | JWT signing secret |

---

## Integration Points

### 1. Customer Creation (User Registration)

When a user registers in Predict, we create/link a License Server customer.

**File:** `packages/be/src/integrations/license-server/customer-service.ts`

```typescript
// Called during user registration or first billing interaction
const customer = await getOrCreateLicenseServerCustomer(userId, email, name);
```

**API Call:**
```
POST /api/portal/customers
Headers:
  X-API-Key: <LICENSE_SERVER_API_KEY>
Body:
  {
    "externalId": "<predict-user-id>",
    "email": "user@example.com",
    "name": "John Doe",
    "source": "predict"
  }
Response:
  {
    "success": true,
    "data": {
      "id": "<license-server-customer-id>",
      "externalId": "<predict-user-id>",
      "email": "user@example.com",
      "stripeCustomerId": "cus_xxx",
      "createdAt": "2026-05-12T00:00:00Z"
    }
  }
```

### 2. Subscription Checkout

**File:** `packages/be/src/api/predict/v1/onboarding/handlers.ts`

```typescript
// Create Stripe checkout session via License Server
const result = await createLicenseCheckout(token, {
  productId: planId,
  billingInterval: 'monthly' | 'annual',
  successUrl,
  cancelUrl,
});
```

**API Call:**
```
POST /api/portal/billing/checkout
Headers:
  Authorization: Bearer <user-jwt>
Body:
  {
    "productId": "<plan-uuid>",
    "billingInterval": "monthly",
    "successUrl": "https://predict.agencio.cloud/onboarding/complete",
    "cancelUrl": "https://predict.agencio.cloud/onboarding/subscribe"
  }
Response:
  {
    "url": "https://checkout.stripe.com/c/pay/..."
  }
```

### 3. Credit Balance & Packages

**File:** `packages/be/src/integrations/license-server/credit-client.ts`

```typescript
// Get user's credit balance
const balance = await getRemoteCreditBalance(authToken);

// List available credit packages
const packages = await listRemoteCreditPackages();

// Create checkout for credit purchase
const checkout = await createRemoteCreditCheckout(
  authToken, packageId, successUrl, cancelUrl
);
```

**API Calls:**
```
GET /api/portal/credits
GET /api/portal/credits/packages
POST /api/portal/credits/checkout
```

### 4. Credit Reserve/Consume Pattern

For AI/LLM calls, we use a reservation pattern to prevent overdraft:

```typescript
// 1. Before LLM call - reserve estimated cost
const reservation = await reserveRemoteCredits(authToken, estimatedCents, idempotencyKey);

// 2. Make LLM call
const result = await callLLM(...);

// 3a. On success - consume actual cost
await consumeRemoteCredits(authToken, reservation.reservationId, actualCents, {
  model: 'claude-3-opus',
  inputTokens: 1000,
  outputTokens: 500,
});

// 3b. On failure - release reservation
await releaseRemoteReservation(authToken, reservation.reservationId);
```

**API Calls:**
```
POST /api/portal/credits/reserve
  Body: { amountCents, idempotencyKey }

POST /api/portal/credits/consume
  Body: { reservationId, amountCents, usage: { model, inputTokens, outputTokens } }

POST /api/portal/credits/release
  Body: { reservationId }
```

### 5. Webhook Processing

License Server sends webhooks to Predict when billing events occur.

**File:** `apps/web/src/app/api/predict/v1/billing/webhooks/license-server/route.ts`

**Endpoint:** `POST /api/predict/v1/billing/webhooks/license-server`

**Events Handled:**
| Event Type | Action |
|------------|--------|
| `checkout.completed` | Activate user subscription |
| `subscription.created` | Record subscription |
| `subscription.updated` | Update subscription status |
| `subscription.deleted` | Mark subscription canceled |
| `credit.purchased` | Sync credit balance |
| `credit.consumed` | Record usage transaction |
| `credit.refunded` | Process refund |
| `credit.balance_updated` | Sync balance from License Server |

**Signature Verification:**
```typescript
const signedPayload = `${timestamp}.${body}`;
const expectedSignature = crypto
  .createHmac('sha256', LICENSE_SERVER_WEBHOOK_SECRET)
  .update(signedPayload)
  .digest('hex');
```

### 6. Deployment Validation

On application startup, Predict validates its deployment license.

**File:** `packages/be/src/lib/deployment-guard/validator.ts`

```typescript
// Called in instrumentation.ts on server startup
await initializeDeploymentGuard({
  strict: process.env.NODE_ENV === 'production',
  enableHeartbeat: true,
});
```

**Validation Flow:**
1. Collect machine fingerprint (hostname, platform, CPUs, MAC addresses)
2. POST to `/api/deployments/validate` with HMAC signature
3. Verify response signature to prevent MITM
4. Check `action` field: `continue`, `warn`, or `kill`
5. Schedule hourly heartbeats

**API Call:**
```
POST /api/deployments/validate
Headers:
  X-Deployment-ID: predict-prod-vercel
  X-Deployment-Signature: <hmac-sha256>
Body:
  {
    "deploymentId": "predict-prod-vercel",
    "machineHash": "<sha256-of-machine-info>",
    "version": "1.6.1",
    "environment": "production",
    "productId": "agencio-predict"
  }
Response (signed):
  {
    "valid": true,
    "action": "continue",
    "tier": "enterprise",
    "_ts": 1715472000000,
    "_did": "predict-prod-vercel",
    "_sig": "<hmac-sha256>"
  }
```

---

## Database Schema (Predict Side)

### `billing.license_server_customers`

Maps Predict users to License Server customers.

```sql
CREATE TABLE billing.license_server_customers (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  license_server_customer_id VARCHAR(255) NOT NULL,
  email VARCHAR(255) NOT NULL,
  stripe_customer_id VARCHAR(255),
  synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  CONSTRAINT unique_user_ls_customer UNIQUE (user_id)
);
```

### `billing.license_server_webhook_events`

Idempotency tracking for webhook events.

```sql
CREATE TABLE billing.license_server_webhook_events (
  id VARCHAR(255) PRIMARY KEY,
  event_type VARCHAR(100) NOT NULL,
  user_id UUID REFERENCES auth.users(id),
  payload JSONB,
  processed BOOLEAN NOT NULL DEFAULT FALSE,
  processing_error TEXT,
  processed_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```

### `billing.ai_credits`

Local cache of credit balance (synced from License Server).

```sql
CREATE TABLE billing.ai_credits (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  license_server_customer_id VARCHAR(255),
  available_cents BIGINT NOT NULL DEFAULT 0,
  reserved_cents BIGINT NOT NULL DEFAULT 0,
  lifetime_purchased_cents BIGINT NOT NULL DEFAULT 0,
  lifetime_used_cents BIGINT NOT NULL DEFAULT 0,
  auto_refill_enabled BOOLEAN NOT NULL DEFAULT FALSE,
  auto_refill_threshold_cents INTEGER,
  updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  CONSTRAINT unique_user_credits UNIQUE (user_id)
);
```

---

## Three Billing Modes

Users can select their billing mode at `/settings/ai-billing`:

### 1. Subscription (Default)

- User has a paid subscription plan
- AI usage included in plan limits
- No per-call charges

### 2. Credits (Pay-Per-Use)

- User purchases credit packages
- Each LLM call deducts from balance
- Auto-refill available when balance drops below threshold

### 3. BYOK (Bring Your Own Key)

- User provides their own OpenAI or Anthropic API key
- Keys encrypted with AES-256-GCM in `billing.user_byok_keys`
- Calls made directly to user's account, no platform charges

---

## File Reference

### Integration Layer (`packages/be/src/integrations/license-server/`)

| File | Purpose |
|------|---------|
| `client.ts` | Portal API client (subscriptions, products, checkout) |
| `credit-client.ts` | Credit operations (balance, reserve, consume, refill) |
| `customer-service.ts` | Customer CRUD, sync balance, auth token generation |
| `webhook-handler.ts` | Process webhook events, signature verification |
| `index.ts` | Barrel exports |

### API Handlers (`packages/be/src/api/predict/v1/`)

| File | Purpose |
|------|---------|
| `onboarding/handlers.ts` | Subscription checkout during onboarding |
| `ai-billing/handlers.ts` | Billing mode, usage, BYOK, credit checkout |
| `credits/handlers.ts` | Credit balance and transactions |

### Routes (`apps/web/src/app/api/predict/v1/`)

| Route | Purpose |
|-------|---------|
| `/billing/webhooks/license-server/route.ts` | Webhook receiver |
| `/ai-billing/*/route.ts` | AI billing endpoints |
| `/credits/*/route.ts` | Credit endpoints |
| `/onboarding/*/route.ts` | Onboarding flow |

### Deployment Guard (`packages/be/src/lib/deployment-guard/`)

| File | Purpose |
|------|---------|
| `index.ts` | Initialization, heartbeat scheduling |
| `validator.ts` | Validation logic, signature verification |

---

## Error Handling

### License Server Unavailable

If the License Server is unreachable:
- Subscription checkout shows "Service unavailable"
- Credit operations fall back to local balance cache
- Deployment validation uses `action: warn` (grace period)

### Webhook Failures

- Events are logged with `processed: false`
- Retry on next webhook delivery from License Server
- Idempotency prevents duplicate processing

### Credit Insufficient

- `reserveCredits` returns `{ success: false, error: 'Insufficient credits' }`
- LLM call is blocked before execution
- User prompted to purchase more credits

---

## Monitoring

### Logs to Watch

```bash
# Webhook processing
grep "License Webhook" /var/log/predict/*.log

# Credit operations
grep "license-server-credit" /var/log/predict/*.log

# Deployment validation
grep "deployment-guard" /var/log/predict/*.log
```

### Health Checks

```bash
# Check License Server connectivity
curl -X GET https://licensing.agencio.cloud/health

# Check webhook endpoint
curl -X POST https://predict.agencio.cloud/api/predict/v1/billing/webhooks/license-server \
  -H "Content-Type: application/json" \
  -d '{"type": "test", "data": {}}'
```

---

## Troubleshooting

### Customer Not Found

If `getOrCreateLicenseServerCustomer` fails:
1. Check `LICENSE_SERVER_URL` is correct
2. Verify `LICENSE_SERVER_API_KEY` matches `ADMIN_API_KEY` on License Server
3. Check network connectivity (VPC peering if applicable)

### Webhook Signature Invalid

If webhooks return 401:
1. Verify `LICENSE_SERVER_WEBHOOK_SECRET` matches on both sides
2. Check timestamp freshness (5-minute max age)
3. Ensure request body is not modified by middleware

### Credits Not Syncing

If local balance doesn't match License Server:
1. Manually trigger sync: `syncCreditBalanceFromLicenseServer(userId)`
2. Check `billing.license_server_webhook_events` for failed events
3. Verify webhook URL is registered on License Server

### Deployment Validation Fails

If deployment validation returns `action: kill`:
1. Check `DEPLOYMENT_KEY` matches on both sides
2. Verify `DEPLOYMENT_ID` is registered on License Server
3. Check license hasn't expired

---

## Security Considerations

1. **API Key Security**: `LICENSE_SERVER_API_KEY` grants server-to-server access. Never expose in client-side code.

2. **Webhook Verification**: Always verify HMAC signature before processing events.

3. **Credit Reservation**: Use idempotency keys to prevent double-charges on retries.

4. **BYOK Encryption**: User API keys are encrypted at rest with AES-256-GCM.

5. **Deployment Signatures**: HMAC-SHA256 signatures on validation requests/responses prevent MITM attacks.

---

## Related Documentation

- `docs/42-ai-billing-credits-plan.md` - AI billing implementation plan
- `docs/41-ai-api-keys-feature-matrix.md` - Platform vs BYOK feature matrix
- `docs/61-deployment-guard.md` - Deployment validation details
- License Server: `docs/predict-integration.md` - License Server side documentation
