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 API key (format: sk-proj-* or sk-*) — used for gpt-* models. Bypasses Formation pool for OpenAI requests.
Anthropic API key (format: sk-ant-*) — used for claude-* models. Bypasses Formation pool for Anthropic requests.
Google Gemini API key (format: AIza*) — used for gemini-* models. Bypasses Formation pool for Google requests.
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
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
}
Whether Formation’s shared OpenRouter pool is available as a fallback. Always true for active accounts.
Whether an OpenAI API key is currently stored.
Whether an Anthropic API key is currently stored.
Whether a Google API key is currently stored.
Whether an OpenRouter API key is currently stored.
ISO 8601 timestamp of when the OpenAI key was last validated. null if no key stored.
ISO 8601 timestamp of when the Anthropic key was last validated. null if no key stored.
ISO 8601 timestamp of when the Google key was last validated. null if no key stored.
openrouter_last_validated
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 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
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:
Stratus receives request with your Stratus API key
Checks for inline headers (X-OpenAI-Key, X-Anthropic-Key, X-Google-Key, X-OpenRouter-Key) — used immediately if present
Checks vault for stored keys — used if found
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
{
"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
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
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.