Skip to content

BYOK for AI Apps: Why Token Reselling is a Broken Business Model

The Problem

I built a Telegram bot that uses GPT-4 to help users with coding questions. The bot worked great in testing, but when I launched it with a subscription model, I hit a wall:

User complaint: "Your bot is too expensive. ChatGPT Plus is $20/month and unlimited. You're charging $15/month for 500 messages."
My costs: 500 messages × $0.03 average = $15 in API costs alone.
My margin: $0.

I realized the economics were impossible. To make profit, I had to markup API costs. But users compared my prices to direct API access or ChatGPT Plus, and my app seemed “expensive.”

Then I found a Reddit thread that articulated this exact problem:

“I figured out another reason why people think AI is less powerful than it actually is. Subscription economics don’t work for reselling AI. The markup required to cover costs makes AI seem expensive or limited.”

What is BYOK?

BYOK (Bring Your Own Key) flips the model:

  • You provide: Interface, workflow, value-added features
  • User provides: API key, pays direct to provider
  • You charge: Subscription for your software value, not token markup

A Reddit comment explained it:

“BYOK: Let users plug in their own API keys. You provide the cool interface/workflow, they pay for their own token usage. The main drawback is that this is too technically complex for most normies.”

Why Token Reselling Fails

I analyzed the failure modes:

Margin Compression

Cost of API call: $0.03
Price to user: $0.05 (to cover overhead + profit)
User perception: "Why pay $0.05 when I can call API directly for $0.03?"

Usage Unpredictability

Power users consume disproportionate tokens:

Normal user: 50 messages/month = $1.50 in API costs
Power user: 2000 messages/month = $60 in API costs
Flat-rate subscription: $10/month
Result: Power users bankrupt you, normal users subsidize them.

Feature Limitations

To control costs, apps artificially limit features:

cost-control.py
class ChatBot:
def __init__(self, user):
self.user = user
self.daily_limit = 20 # Artificial limit to control costs
async def chat(self, message):
if await self.user.daily_count() >= self.daily_limit:
return "Daily limit reached. Upgrade to Pro!" # User frustration
# ...

Users perceive AI as less capable because of artificial limits.

BYOK Implementation

I rebuilt my app with BYOK. Here’s the architecture.

Database Schema

models.py
from sqlalchemy import Column, Integer, String, DateTime
from datetime import datetime
class UserApiKey(Base):
__tablename__ = 'user_api_keys'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, nullable=False, index=True)
provider = Column(String(50), nullable=False) # 'openai', 'anthropic'
encrypted_key = Column(String(500), nullable=False)
key_hint = Column(String(20)) # Last 4 chars for display
created_at = Column(DateTime, default=datetime.utcnow)
last_used_at = Column(DateTime, nullable=True)

Secure Key Storage

The critical security requirement: never store API keys in plain text.

encryption.py
from cryptography.fernet import Fernet
import os
# Generate once, store in environment variable
# NEVER commit this to git!
ENCRYPTION_KEY = os.environ.get('API_KEY_ENCRYPTION_KEY')
fernet = Fernet(ENCRYPTION_KEY)
def encrypt_api_key(api_key: str) -> str:
return fernet.encrypt(api_key.encode()).decode()
def decrypt_api_key(encrypted_key: str) -> str:
return fernet.decrypt(encrypted_key.encode()).decode()
def get_key_hint(api_key: str) -> str:
"""Show last 4 characters for user identification"""
return f"...{api_key[-4:]}"

To generate the encryption key:

Terminal window
# Generate a Fernet key (run once)
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
# Add to .env (add .env to .gitignore!)
API_KEY_ENCRYPTION_KEY=your-generated-key-here

Key Validation Endpoint

Always validate keys before storing:

routes/api_keys.py
from fastapi import APIRouter, HTTPException
from anthropic import Anthropic
from openai import OpenAI
from slowapi import Limiter
router = APIRouter()
limiter = Limiter(key_func=get_user_id)
@router.post("/api/keys/validate")
@limiter.limit("5/minute") # Prevent brute force
async def validate_api_key(provider: str, api_key: str):
"""Validate API key before storing"""
try:
if provider == "anthropic":
client = Anthropic(api_key=api_key)
# Minimal API call to validate
client.messages.create(
model="claude-3-haiku-20240307",
max_tokens=10,
messages=[{"role": "user", "content": "Hi"}]
)
elif provider == "openai":
client = OpenAI(api_key=api_key)
client.models.list()
return {"valid": True, "hint": get_key_hint(api_key)}
except Exception as e:
raise HTTPException(
status_code=400,
detail=f"Invalid API key: {str(e)}"
)

Key Storage Endpoint

routes/api_keys.py
@router.post("/api/keys")
async def store_api_key(
provider: str,
api_key: str,
user_id: int = Depends(get_current_user_id)
):
# Validate first
validation = await validate_api_key(provider, api_key)
if not validation["valid"]:
raise HTTPException(400, "Invalid API key")
# Encrypt and store
encrypted = encrypt_api_key(api_key)
existing = await db.get_user_key(user_id, provider)
if existing:
# Update existing key
await db.update(
UserApiKey,
{"user_id": user_id, "provider": provider},
{"encrypted_key": encrypted, "key_hint": validation["hint"]}
)
else:
# Create new key
await db.insert(
UserApiKey(
user_id=user_id,
provider=provider,
encrypted_key=encrypted,
key_hint=validation["hint"]
)
)
return {"success": True, "hint": validation["hint"]}

Using User Keys for API Calls

api_client.py
async def get_api_client(user_id: int, provider: str):
"""Get API client with user's key"""
user_key = await db.get_user_key(user_id, provider)
if not user_key:
raise HTTPException(
402,
"No API key configured. Add your key in Settings."
)
decrypted_key = decrypt_api_key(user_key.encrypted_key)
if provider == "anthropic":
return Anthropic(api_key=decrypted_key)
elif provider == "openai":
return OpenAI(api_key=decrypted_key)

Frontend Key Management

Using Alpine.js + Tailwind (per project requirements):

settings.html
<div x-data="apiKeyManager()">
<h3>API Keys</h3>
<template x-for="provider in providers" :key="provider.id">
<div class="border rounded p-4 mb-4">
<div class="flex justify-between items-center">
<div>
<h4 x-text="provider.name"></h4>
<p class="text-sm text-gray-500"
x-show="keys[provider.id]"
x-text="'Key: ' + keys[provider.id]?.hint">
</p>
</div>
<button @click="selectedProvider = provider.id"
class="btn btn-primary">
<span x-text="keys[provider.id] ? 'Update' : 'Add Key'"></span>
</button>
</div>
</div>
</template>
<!-- Modal for key input -->
<div x-show="selectedProvider" class="modal">
<input type="password"
x-model="newKey"
placeholder="Paste your API key"
class="input" />
<p class="text-sm text-gray-500"
x-text="'Get your key from ' + getProviderDocs(selectedProvider)">
</p>
<button @click="saveKey()"
:disabled="saving"
class="btn btn-primary">
<span x-text="saving ? 'Validating...' : 'Save'"></span>
</button>
<p x-show="error" x-text="error" class="text-red-500"></p>
</div>
</div>
<script>
function apiKeyManager() {
return {
providers: [
{ id: 'anthropic', name: 'Claude (Anthropic)', docs: 'https://console.anthropic.com' },
{ id: 'openai', name: 'OpenAI', docs: 'https://platform.openai.com/api-keys' }
],
keys: {},
selectedProvider: null,
newKey: '',
saving: false,
error: null,
async init() {
// Load existing keys
const response = await fetch('/api/keys');
this.keys = await response.json();
},
getProviderDocs(providerId) {
return this.providers.find(p => p.id === providerId)?.docs || '';
},
async saveKey() {
if (!this.newKey || !this.selectedProvider) return;
this.saving = true;
this.error = null;
try {
const response = await fetch('/api/keys', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
provider: this.selectedProvider,
api_key: this.newKey
})
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.detail || 'Failed to save key');
}
const data = await response.json();
this.keys[this.selectedProvider] = { hint: data.hint };
this.selectedProvider = null;
this.newKey = '';
} catch (err) {
this.error = err.message;
} finally {
this.saving = false;
}
}
}
}
</script>

Security Checklist

After implementing BYOK, I created a security checklist:

security_checklist.py
# 1. NEVER log API keys
import logging
logging.getLogger('httpx').setLevel(logging.WARNING) # Redact from HTTP logs
# 2. Use environment variables for encryption keys
# NEVER commit encryption keys to git
# .env file (add to .gitignore):
# API_KEY_ENCRYPTION_KEY=your-fernet-key-here
# 3. Implement key rotation support
@router.post("/api/keys/rotate")
async def rotate_key(user_id: int, provider: str, new_key: str):
# Validate new key first
# Then replace old key atomically
pass
# 4. Rate limit key validation attempts
from slowapi import Limiter
limiter = Limiter(key_func=get_user_id)
@router.post("/api/keys/validate")
@limiter.limit("5/minute") # Prevent brute force
async def validate_api_key(...):
pass
# 5. Audit trail for key operations
class KeyAuditLog(Base):
__tablename__ = 'key_audit_log'
id = Column(Integer, primary_key=True)
user_id = Column(Integer, nullable=False)
action = Column(String(50)) # 'create', 'update', 'delete', 'validate'
provider = Column(String(50))
timestamp = Column(DateTime, default=datetime.utcnow)
ip_address = Column(String(50))

Hybrid Model

The Reddit thread had a question:

“Is it possible to set up SaaS and have people log in to their own AI subscription? That way you can have the best of both worlds?”

Yes. Here’s the hybrid approach:

hybrid_client.py
async def get_api_client(user_id: int, provider: str):
"""Hybrid: user key OR app key"""
# First, check for user's own key
user_key = await get_user_key(user_id, provider)
if user_key:
# User's own key - no cost to app
return create_client(provider, decrypt_api_key(user_key.encrypted_key))
# Fall back to app's key (with usage tracking)
if await check_app_key_quota(user_id):
return create_client(provider, get_app_key(provider))
raise HTTPException(
402,
"Usage quota exceeded. Add your own API key for unlimited access."
)

Pricing Tiers

Free Tier:
- BYOK only
- User provides their own API key
- Unlimited usage (user pays API costs directly)
Pro Tier ($10/month):
- Optional: Use app's API key with quota
- Or: BYOK for unlimited access
- Premium features
Enterprise ($50+/month):
- Managed keys with markup
- Dedicated support
- Compliance features

Trade-offs

After implementing BYOK, I found:

Pros:

  • High margins (100% software margin, no API costs)
  • Transparent pricing (users see what they pay for)
  • No usage risk (power users don’t bankrupt you)
  • User trust (data goes direct to provider)

Cons:

  • Limited to technical users
  • Smaller total addressable market
  • Users must obtain and manage API keys
  • Support burden for key issues

Model Comparison

AspectTraditional ResellerBYOK ModelHybrid Model
PricingComplex usage tiersSimple subscriptionTiered options
MarginsSqueezed by API costs100% software marginMixed
User TrustData passes through youDirect provider connectionUser choice
Scale LimitLimited by token costsUnlimitedQuota + BYOK
AudienceMass marketTechnical usersBoth

Summary

In this post, I explained why BYOK is the sustainable business model for AI applications. The key point is that token reselling has impossible economics: you must markup API costs to make profit, but users compare your prices to direct API access and perceive your app as expensive.

BYOK removes this problem entirely. You focus on building valuable software, not managing usage margins. The trade-off is a smaller, more technical audience, but this audience often has higher willingness to pay for quality tools.

If you’re building an AI app, ask yourself: are you selling API access (race to the bottom on price) or software value (defensible differentiation)?

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments