> ## Documentation Index
> Fetch the complete documentation index at: https://www.stratus.run/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# LLM Key Management

> Securely store and manage user LLM provider keys

## Overview

The LLM Key Management endpoints let you securely store encrypted LLM provider API keys in your Stratus account. These are **optional** — Formation provides a shared OpenRouter pool that handles all requests automatically when no native key is configured.

Storing your own key bypasses the Formation pool entirely: you pay your provider directly and the 25% pool markup is not applied.

<Info>
  **Key Resolution Priority:** Stratus checks for keys in this order: (1) inline request headers, (2) vault-stored keys, (3) Formation's pool. See [Authentication - Key Resolution Priority](/docs/authentication#key-resolution-priority) for a full breakdown.
</Info>

## Why Store Your Own Keys?

<CardGroup cols={2}>
  <Card title="No Markup" icon="dollar-sign">
    Bypass the Formation pool. Pay providers directly at your own rates — no 25% markup.
  </Card>

  <Card title="Privacy" icon="lock">
    Your keys are encrypted at rest using AES-256-GCM and never leave encrypted storage.
  </Card>

  <Card title="Secure Storage" icon="shield">
    Keys stored in Supabase Vault and validated on write.
  </Card>

  <Card title="Convenience" icon="sparkles">
    Store once, used automatically on every request — no need to pass headers.
  </Card>
</CardGroup>

## Security Architecture

### Encryption

* **Algorithm:** AES-256-GCM (Galois/Counter Mode)
* **Storage:** Supabase Vault (encrypted at rest)
* **Validation:** Keys validated on storage (attempted API call to provider)
* **Caching:** 5-minute in-memory cache for performance

### Key Lifecycle

```
1. User provides LLM keys → 2. Stratus validates → 3. Encrypt with AES-256-GCM → 4. Store in Vault
                                                                                          ↓
5. User makes request → 6. Retrieve from cache/vault → 7. Decrypt → 8. Use to call LLM provider
```

<Warning>
  **Vault Connection Required:** These endpoints require active Vault connection. Check `/health` endpoint to verify `vault: "connected"` before use.
</Warning>

## Endpoints

### Store LLM Keys

```
POST /v1/account/llm-keys
```

Store encrypted LLM provider API keys. All fields are optional — provide any combination of the providers you want to use. Omitted keys remain unchanged.

#### Request

```json theme={null}
{
  "openai_key": "sk-proj-...",
  "anthropic_key": "sk-ant-...",
  "google_key": "AIza...",
  "openrouter_key": "sk-or-..."
}
```

<ParamField body="openai_key" type="string">
  OpenAI API key (format: `sk-proj-*` or `sk-*`) — used for `gpt-*` models. Bypasses Formation pool for OpenAI requests.
</ParamField>

<ParamField body="anthropic_key" type="string">
  Anthropic API key (format: `sk-ant-*`) — used for `claude-*` models. Bypasses Formation pool for Anthropic requests.
</ParamField>

<ParamField body="google_key" type="string">
  Google Gemini API key (format: `AIza*`) — used for `gemini-*` models. Bypasses Formation pool for Google requests.
</ParamField>

<ParamField body="openrouter_key" type="string">
  OpenRouter API key (format: `sk-or-*`) — used as a native BYOK OpenRouter key. Bypasses Formation's pool key entirely.
</ParamField>

<Note>
  All fields are optional. You can supply any combination. Omitted keys remain unchanged (or unset if this is your first call). If no keys are stored, Formation's shared pool handles requests automatically.
</Note>

#### Response

```json theme={null}
{
  "success": true,
  "message": "LLM API keys stored and validated successfully"
}
```

#### Example

<CodeGroup>
  ```typescript TypeScript theme={null}
  const response = await fetch('https://api.stratus.run/v1/account/llm-keys', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.STRATUS_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      openai_key: 'sk-proj-your-openai-key',       // optional
      anthropic_key: 'sk-ant-your-anthropic-key',  // optional
      google_key: 'AIza-your-google-key',           // optional
      openrouter_key: 'sk-or-your-openrouter-key', // optional
    })
  });

  const result = await response.json();
  console.log(result.message); // "LLM API keys stored and validated successfully"
  ```

  ```python Python theme={null}
  import requests

  response = requests.post(
      "https://api.stratus.run/v1/account/llm-keys",
      headers={
          "Authorization": f"Bearer {os.environ['STRATUS_API_KEY']}",
          "Content-Type": "application/json"
      },
      json={
          "openai_key": "sk-proj-your-openai-key",       # optional
          "anthropic_key": "sk-ant-your-anthropic-key",  # optional
          "google_key": "AIza-your-google-key",           # optional
          "openrouter_key": "sk-or-your-openrouter-key", # optional
      }
  )

  result = response.json()
  print(result['message'])  # "LLM API keys stored and validated successfully"
  ```

  ```bash cURL theme={null}
  curl -X POST https://api.stratus.run/v1/account/llm-keys \
    -H "Authorization: Bearer stratus_sk_live_your_key" \
    -H "Content-Type: application/json" \
    -d '{
      "openai_key": "sk-proj-your-openai-key",
      "anthropic_key": "sk-ant-your-anthropic-key",
      "google_key": "AIza-your-google-key",
      "openrouter_key": "sk-or-your-openrouter-key"
    }'
  ```
</CodeGroup>

***

### Get Key Status

```
GET /v1/account/llm-keys
```

Check which LLM keys are configured (does not return actual keys).

#### Request

No body required. Authentication via `Authorization` header.

#### Response

```json theme={null}
{
  "formation_keys_available": true,
  "has_openai_key": true,
  "has_anthropic_key": false,
  "has_google_key": false,
  "has_openrouter_key": false,
  "openai_last_validated": "2026-02-12T22:30:00Z",
  "anthropic_last_validated": null,
  "google_last_validated": null,
  "openrouter_last_validated": null
}
```

<ResponseField name="formation_keys_available" type="boolean">
  Whether Formation's shared OpenRouter pool is available as a fallback. Always `true` for active accounts.
</ResponseField>

<ResponseField name="has_openai_key" type="boolean">
  Whether an OpenAI API key is currently stored.
</ResponseField>

<ResponseField name="has_anthropic_key" type="boolean">
  Whether an Anthropic API key is currently stored.
</ResponseField>

<ResponseField name="has_google_key" type="boolean">
  Whether a Google API key is currently stored.
</ResponseField>

<ResponseField name="has_openrouter_key" type="boolean">
  Whether an OpenRouter API key is currently stored.
</ResponseField>

<ResponseField name="openai_last_validated" type="string | null">
  ISO 8601 timestamp of when the OpenAI key was last validated. `null` if no key stored.
</ResponseField>

<ResponseField name="anthropic_last_validated" type="string | null">
  ISO 8601 timestamp of when the Anthropic key was last validated. `null` if no key stored.
</ResponseField>

<ResponseField name="google_last_validated" type="string | null">
  ISO 8601 timestamp of when the Google key was last validated. `null` if no key stored.
</ResponseField>

<ResponseField name="openrouter_last_validated" type="string | null">
  ISO 8601 timestamp of when the OpenRouter key was last validated. `null` if no key stored.
</ResponseField>

#### Example

<CodeGroup>
  ```typescript TypeScript theme={null}
  const response = await fetch('https://api.stratus.run/v1/account/llm-keys', {
    headers: {
      'Authorization': `Bearer ${process.env.STRATUS_API_KEY}`
    }
  });

  const status = await response.json();
  console.log('OpenAI key configured:', status.has_openai_key);
  console.log('Anthropic key configured:', status.has_anthropic_key);
  console.log('OpenAI last validated:', status.openai_last_validated);
  ```

  ```python Python theme={null}
  import requests

  response = requests.get(
      "https://api.stratus.run/v1/account/llm-keys",
      headers={"Authorization": f"Bearer {os.environ['STRATUS_API_KEY']}"}
  )

  status = response.json()
  print(f"OpenAI key configured: {status['has_openai_key']}")
  print(f"Anthropic key configured: {status['has_anthropic_key']}")
  print(f"OpenAI last validated: {status['openai_last_validated']}")
  ```

  ```bash cURL theme={null}
  curl https://api.stratus.run/v1/account/llm-keys \
    -H "Authorization: Bearer stratus_sk_live_your_key"
  ```
</CodeGroup>

***

### Delete LLM Keys

```
DELETE /v1/account/llm-keys
```

Remove stored LLM keys from vault.

#### Request

Use the optional `provider` query parameter to delete a specific provider's key. Omit to delete all keys.

<ParamField query="provider" type="string">
  Provider to delete: `openai`, `anthropic`, `google`, or `openrouter`.

  **If omitted:** Deletes all stored LLM keys.
</ParamField>

#### Response

```json theme={null}
{
  "success": true,
  "message": "LLM keys deleted"
}
```

#### Example

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Delete all keys
  const response = await fetch('https://api.stratus.run/v1/account/llm-keys', {
    method: 'DELETE',
    headers: {
      'Authorization': `Bearer ${process.env.STRATUS_API_KEY}`
    }
  });

  // Delete specific provider (query param)
  const response = await fetch('https://api.stratus.run/v1/account/llm-keys?provider=openai', {
    method: 'DELETE',
    headers: {
      'Authorization': `Bearer ${process.env.STRATUS_API_KEY}`
    }
  });

  const result = await response.json();
  console.log(result.success); // true
  ```

  ```python Python theme={null}
  import requests

  # Delete all keys
  response = requests.delete(
      "https://api.stratus.run/v1/account/llm-keys",
      headers={"Authorization": f"Bearer {os.environ['STRATUS_API_KEY']}"}
  )

  # Delete specific provider (query param)
  response = requests.delete(
      "https://api.stratus.run/v1/account/llm-keys",
      params={"provider": "openai"},
      headers={"Authorization": f"Bearer {os.environ['STRATUS_API_KEY']}"}
  )

  result = response.json()
  print(result['success'])  # True
  ```

  ```bash cURL theme={null}
  # Delete all keys
  curl -X DELETE https://api.stratus.run/v1/account/llm-keys \
    -H "Authorization: Bearer stratus_sk_live_your_key"

  # Delete specific provider (query param)
  curl -X DELETE "https://api.stratus.run/v1/account/llm-keys?provider=openai" \
    -H "Authorization: Bearer stratus_sk_live_your_key"
  ```
</CodeGroup>

## Usage Workflow

### Zero-Config Flow (Formation Pool)

No setup required. Make your first call immediately after getting a Stratus API key:

```typescript theme={null}
// Works out of the box — Formation pool handles the LLM call
const response = await fetch('https://api.stratus.run/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${userStratusKey}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    model: 'stratus-x1ac-small-gpt-4o',
    messages: [
      { role: 'system', content: 'Current state: ...' },
      { role: 'user', content: 'What happens if I click submit?' }
    ]
  })
});

const data = await response.json();
console.log(data.stratus.key_source);              // "formation"
console.log(data.stratus.formation_markup_applied); // 0.25
```

### BYOK Setup Flow (Remove Markup)

```typescript theme={null}
// 1. Check vault connection (optional — for resilience)
const health = await fetch('https://api.stratus.run/health').then(r => r.json());
if (health.vault !== 'connected') {
  console.warn('Vault unavailable - keys will use Formation pool fallback');
}

// 2. Store user's LLM keys
await fetch('https://api.stratus.run/v1/account/llm-keys', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${userStratusKey}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    openai_key: userOpenAiKey,       // optional
    anthropic_key: userAnthropicKey, // optional
  })
});

// 3. Verify storage
const status = await fetch('https://api.stratus.run/v1/account/llm-keys', {
  headers: { 'Authorization': `Bearer ${userStratusKey}` }
}).then(r => r.json());

console.log('Formation pool available:', status.formation_keys_available); // true
console.log('Has OpenAI key:', status.has_openai_key);
console.log('Has Anthropic key:', status.has_anthropic_key);

// 4. Make predictions — stored key used automatically, no markup
const response = await fetch('https://api.stratus.run/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${userStratusKey}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    model: 'stratus-x1ac-small-gpt-4o',
    messages: [
      { role: 'system', content: 'Current state: ...' },
      { role: 'user', content: 'What happens if I click submit?' }
    ]
  })
});

const data = await response.json();
console.log(data.stratus.key_source);              // "user"
console.log(data.stratus.formation_markup_applied); // null
```

### Key Rotation

```typescript theme={null}
async function rotateOpenAiKey(stratusKey: string, newOpenAiKey: string) {
  // 1. Store new key
  await fetch('https://api.stratus.run/v1/account/llm-keys', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${stratusKey}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ openai_key: newOpenAiKey })
  });

  // 2. Verify
  const status = await fetch('https://api.stratus.run/v1/account/llm-keys', {
    headers: { 'Authorization': `Bearer ${stratusKey}` }
  }).then(r => r.json());

  if (status.has_openai_key) {
    console.log('OpenAI key rotated successfully');
  }
}
```

### User Onboarding UI

```typescript theme={null}
async function onboardUserLLMKeys(stratusKey: string) {
  // LLM keys are optional — Formation pool works out of the box.
  // Offer this as an upgrade path to remove the 25% markup.
  const openaiKey = await promptUser('Enter your OpenAI API key (optional — removes pool markup):');
  const anthropicKey = await promptUser('Enter your Anthropic API key (optional):');

  if (!openaiKey && !anthropicKey) {
    // Totally fine — Formation pool will be used automatically
    console.log('No keys provided. Formation pool will handle requests (25% markup applies).');
    return;
  }

  // Store provided keys
  const response = await fetch('https://api.stratus.run/v1/account/llm-keys', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${stratusKey}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      ...(openaiKey && { openai_key: openaiKey }),
      ...(anthropicKey && { anthropic_key: anthropicKey })
    })
  });

  const result = await response.json();

  if (result.success) {
    console.log('Keys stored securely. Pool markup removed for stored providers.');
  } else {
    console.error('Failed to store keys');
  }
}
```

## How It Works with Predictions

When you make a prediction request, Stratus resolves the LLM key in priority order:

1. **Stratus receives request** with your Stratus API key
2. **Checks for inline headers** (`X-OpenAI-Key`, `X-Anthropic-Key`, `X-Google-Key`, `X-OpenRouter-Key`) — used immediately if present
3. **Checks vault for stored keys** — used if found
4. **Falls back to Formation pool** — Formation's shared OpenRouter key is used; a 25% markup is applied to the credit cost

```typescript theme={null}
// Scenario A: user has stored their OpenAI key (no markup)
const response = await fetch('https://api.stratus.run/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${stratusKey}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    model: 'stratus-x1ac-small-gpt-4o',
    messages: [...]
  })
});
// response.stratus.key_source === "user"
// response.stratus.formation_markup_applied === null

// Scenario B: no keys stored — Formation pool activates automatically
const response = await fetch('https://api.stratus.run/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${stratusKey}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    model: 'stratus-x1ac-small-gpt-4o',
    messages: [...]
  })
});
// response.stratus.key_source === "formation"
// response.stratus.formation_markup_applied === 0.25
```

<Info>
  **Cost transparency:** The `key_source` and `formation_markup_applied` fields in every response tell you exactly how the request was billed.
</Info>

## Error Handling

### Invalid Key Format

```json theme={null}
{
  "error": "invalid_request",
  "message": "Invalid OpenAI API key format"
}
```

**Solution:** Verify key format matches provider requirements:

* OpenAI: `sk-proj-*` or `sk-*`
* Anthropic: `sk-ant-*`
* Google: `AIza*`
* OpenRouter: `sk-or-*`

### Key Validation Failed

```json theme={null}
{
  "error": "validation_failed",
  "message": "OpenAI API key is invalid or inactive",
  "provider": "openai"
}
```

**Cause:** Key was rejected by provider during validation test

**Solution:**

* Verify key is active in provider dashboard
* Check key has required permissions
* Ensure no rate limits or billing issues

### Vault Unavailable

```json theme={null}
{
  "error": "service_unavailable",
  "message": "Vault connection unavailable - cannot store keys"
}
```

**Solution:** Check `/health` endpoint for vault status. Wait for vault to reconnect or use alternative method (pass keys directly in requests).

### Authentication Failed

```json theme={null}
{
  "error": "authentication_failed",
  "message": "Invalid Stratus API key"
}
```

**Solution:** Verify your Stratus API key is correct and active.

## Security Best Practices

### 1. Validate Before Storage

Always validate keys work before storing:

```typescript theme={null}
async function validateAndStoreKeys(
  stratusKey: string,
  openaiKey?: string,
  anthropicKey?: string
) {
  // Validate OpenAI key
  if (openaiKey) {
    try {
      const testResponse = await fetch('https://api.openai.com/v1/models', {
        headers: { 'Authorization': `Bearer ${openaiKey}` }
      });
      if (!testResponse.ok) {
        throw new Error('Invalid OpenAI key');
      }
    } catch (error) {
      console.error('OpenAI key validation failed:', error);
      return false;
    }
  }

  // Store if valid
  const response = await fetch('https://api.stratus.run/v1/account/llm-keys', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${stratusKey}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      ...(openaiKey && { openai_key: openaiKey }),
      ...(anthropicKey && { anthropic_key: anthropicKey })
    })
  });

  return response.ok;
}
```

### 2. Never Log or Expose Keys

```typescript theme={null}
// ❌ Don't
console.log('Storing key:', openaiKey);
sendToAnalytics({ openai_key: openaiKey });

// ✅ Do
console.log('Storing OpenAI key');
sendToAnalytics({ has_openai_key: !!openaiKey });
```

### 3. Use HTTPS Always

```typescript theme={null}
// ❌ Don't
fetch('http://api.stratus.run/v1/account/llm-keys', ...)

// ✅ Do
fetch('https://api.stratus.run/v1/account/llm-keys', ...)
```

### 4. Implement Key Expiry Checks

```typescript theme={null}
async function checkKeyStatus(stratusKey: string) {
  const status = await fetch('https://api.stratus.run/v1/account/llm-keys', {
    headers: { 'Authorization': `Bearer ${stratusKey}` }
  }).then(r => r.json());

  if (status.openai_last_validated) {
    const lastValidated = new Date(status.openai_last_validated);
    const daysSince = (Date.now() - lastValidated.getTime()) / (1000 * 60 * 60 * 24);

    if (daysSince > 90) {
      console.warn('OpenAI key is >90 days old - consider rotation');
    }
  }
}
```

## Comparison: Key Supply Methods

| Method                | Markup | Storage                 | Convenience             | Use Case                           |
| --------------------- | ------ | ----------------------- | ----------------------- | ---------------------------------- |
| **Formation pool**    | 25%    | None                    | Automatic (zero config) | Default, prototyping, quick starts |
| **Inline headers**    | None   | Not stored              | Per-request headers     | Testing, CI, multi-key setups      |
| **Vault-stored keys** | None   | Encrypted (AES-256-GCM) | Set once, automatic     | Production BYOK                    |

### Inline Headers Example

Pass your key directly per-request at the highest priority. All four providers are supported:

```typescript theme={null}
const response = await fetch('https://api.stratus.run/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${stratusKey}`,
    'X-OpenAI-Key': openaiKey,           // OpenAI — takes priority over vault + pool
    'X-Anthropic-Key': anthropicKey,     // Anthropic
    'X-Google-Key': googleKey,           // Google Gemini
    'X-OpenRouter-Key': openrouterKey,   // OpenRouter
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    model: 'stratus-x1ac-small-gpt-4o',
    messages: [...]
  })
});
```

<Warning>
  **Security Note:** Passing keys directly in headers exposes them in logs and network traffic. Use stored keys for production.
</Warning>

## Related Endpoints

* [Health Check](/docs/api-reference/health) - Verify vault connection before storing keys
* [Chat Completions](/docs/api-reference/chat-completions) - Make predictions using stored keys
* [Messages](/docs/api-reference/messages) - Anthropic-format predictions using stored keys

<Card title="Need Help?" icon="question">
  Questions about LLM key management? Contact [support@stratus.run](mailto:support@stratus.run)
</Card>
