Skip to main content
POST
/
v1
/
account
/
llm-keys
LLM Key Management
curl --request POST \
  --url https://api.stratus.run/v1/account/llm-keys \
  --header 'Content-Type: application/json' \
  --data '
{
  "openai_key": "<string>",
  "anthropic_key": "<string>",
  "google_key": "<string>",
  "openrouter_key": "<string>"
}
'
{
  "formation_keys_available": true,
  "has_openai_key": true,
  "has_anthropic_key": true,
  "has_google_key": true,
  "has_openrouter_key": true,
  "openai_last_validated": {},
  "anthropic_last_validated": {},
  "google_last_validated": {},
  "openrouter_last_validated": {}
}

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.
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 for a full breakdown.

Why Store Your Own Keys?

No Markup

Bypass the Formation pool. Pay providers directly at your own rates — no 25% markup.

Privacy

Your keys are encrypted at rest using AES-256-GCM and never leave encrypted storage.

Secure Storage

Keys stored in Supabase Vault and validated on write.

Convenience

Store once, used automatically on every request — no need to pass headers.

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
Vault Connection Required: These endpoints require active Vault connection. Check /health endpoint to verify vault: "connected" before use.

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

{
  "openai_key": "sk-proj-...",
  "anthropic_key": "sk-ant-...",
  "google_key": "AIza...",
  "openrouter_key": "sk-or-..."
}
openai_key
string
OpenAI API key (format: sk-proj-* or sk-*) — used for gpt-* models. Bypasses Formation pool for OpenAI requests.
anthropic_key
string
Anthropic API key (format: sk-ant-*) — used for claude-* models. Bypasses Formation pool for Anthropic requests.
google_key
string
Google Gemini API key (format: AIza*) — used for gemini-* models. Bypasses Formation pool for Google requests.
openrouter_key
string
OpenRouter API key (format: sk-or-*) — used as a native BYOK OpenRouter key. Bypasses Formation’s pool key entirely.
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.

Response

{
  "success": true,
  "message": "LLM API keys stored and validated successfully"
}

Example

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"

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

{
  "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
}
formation_keys_available
boolean
Whether Formation’s shared OpenRouter pool is available as a fallback. Always true for active accounts.
has_openai_key
boolean
Whether an OpenAI API key is currently stored.
has_anthropic_key
boolean
Whether an Anthropic API key is currently stored.
has_google_key
boolean
Whether a Google API key is currently stored.
has_openrouter_key
boolean
Whether an OpenRouter API key is currently stored.
openai_last_validated
string | null
ISO 8601 timestamp of when the OpenAI key was last validated. null if no key stored.
anthropic_last_validated
string | null
ISO 8601 timestamp of when the Anthropic key was last validated. null if no key stored.
google_last_validated
string | null
ISO 8601 timestamp of when the Google key was last validated. null if no key stored.
openrouter_last_validated
string | null
ISO 8601 timestamp of when the OpenRouter key was last validated. null if no key stored.

Example

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);

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.
provider
string
Provider to delete: openai, anthropic, google, or openrouter.If omitted: Deletes all stored LLM keys.

Response

{
  "success": true,
  "message": "LLM keys deleted"
}

Example

// 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

Usage Workflow

Zero-Config Flow (Formation Pool)

No setup required. Make your first call immediately after getting a Stratus API key:
// 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)

// 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

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

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
// 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
Cost transparency: The key_source and formation_markup_applied fields in every response tell you exactly how the request was billed.

Error Handling

Invalid Key Format

{
  "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

{
  "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

{
  "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

{
  "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:
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

// ❌ 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

// ❌ 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

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

MethodMarkupStorageConvenienceUse Case
Formation pool25%NoneAutomatic (zero config)Default, prototyping, quick starts
Inline headersNoneNot storedPer-request headersTesting, CI, multi-key setups
Vault-stored keysNoneEncrypted (AES-256-GCM)Set once, automaticProduction BYOK

Inline Headers Example

Pass your key directly per-request at the highest priority. All four providers are supported:
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: [...]
  })
});
Security Note: Passing keys directly in headers exposes them in logs and network traffic. Use stored keys for production.

Need Help?

Questions about LLM key management? Contact support@stratus.run