Drop OpenRouter: self-host the OVTH Gateway
A migration guide from OpenRouter to a self-hosted Gateway. Same OpenAI-compatible API, same models, half the latency, none of the markup.
Who this is for
You burn more than $50/mo on OpenRouter and have noticed that 5% markup is starting to show on the invoice. You already have keys for the underlying labs (Anthropic, OpenAI, Gemini). You are okay running a small Docker container on a box you own. You want one endpoint for every model and you want it to be yours.
If you are under $50/mo, stay on OpenRouter — the engineering time to migrate is not worth the arbitrage.
The migration, one diff
- OPENAI_API_BASE=https://openrouter.ai/api/v1
- OPENAI_API_KEY=sk-or-v1-...
+ OPENAI_API_BASE=https://gateway.your-domain.dev/v1
+ OPENAI_API_KEY=ovth_... That is the whole change for 90% of clients. SDKs, CLIs, IDEs — Cursor, Aider, OpenCode, Continue, Claude Code (via ANTHROPIC_BASE_URL) — all speak this shape.
Tools and versions
- A VPS with 1GB RAM, anywhere (Hetzner CX11 is €4/mo, we run one)
- Docker 24+ and docker compose
- Caddy or any reverse proxy that terminates TLS
- OVTH Gateway 0.9.x (Docker image:
ghcr.io/nousresearch/ovth-gateway) - Your existing provider keys
Setup in five steps
01. Provision the box
# Hetzner or your VPS of choice
ssh root@your-box
curl -fsSL https://get.docker.com | sh
mkdir -p /srv/ovth && cd /srv/ovth 02. Compose the Gateway
# /srv/ovth/docker-compose.yml
services:
gateway:
image: ghcr.io/nousresearch/ovth-gateway:0.9
restart: always
ports: ["127.0.0.1:8080:8080"]
env_file: .env
volumes:
- ./data:/data
- ./config.yaml:/app/config.yaml:ro # /srv/ovth/config.yaml
routes:
auto:
strategy: cheapest-capable
providers: [anthropic, openai, google]
cheap:
providers: [google, anthropic]
models: [gemini-1.5-flash, claude-haiku-4]
smart:
providers: [anthropic, openai]
models: [claude-opus-4.7, gpt-5]
quota:
daily_tokens: 500000
per_user: true # /srv/ovth/.env
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
GOOGLE_API_KEY=...
OVTH_MASTER_KEY=$(openssl rand -hex 32) 03. Terminate TLS with Caddy
# /etc/caddy/Caddyfile
gateway.your-domain.dev {
reverse_proxy 127.0.0.1:8080
encode gzip zstd
header {
Strict-Transport-Security "max-age=31536000"
X-Frame-Options "DENY"
}
} docker compose up -d
systemctl reload caddy
curl https://gateway.your-domain.dev/v1/models | jq '.data | length'
# → 28 or so 04. Mint a per-app key and flip your clients
curl -X POST https://gateway.your-domain.dev/admin/keys \
-H "Authorization: Bearer $OVTH_MASTER_KEY" \
-d '{"label":"cursor","quota":{"daily_tokens":100000}}'
# → {"key":"ovth_live_..."} Rotate this key whenever. One Gateway, many apps, independent quotas.
05. Verify with a drop-in curl against both APIs
# OpenAI shape
curl https://gateway.your-domain.dev/v1/chat/completions \
-H "Authorization: Bearer ovth_live_..." \
-d '{"model":"auto","messages":[{"role":"user","content":"hi"}]}'
# Anthropic shape — same gateway, different path
curl https://gateway.your-domain.dev/anthropic/v1/messages \
-H "x-api-key: ovth_live_..." \
-H "anthropic-version: 2023-06-01" \
-d '{"model":"claude-sonnet-4.5","max_tokens":50,"messages":[{"role":"user","content":"hi"}]}' Both should return a real model response. If they do, you are done. Flip the env vars in your clients.
Cost, privacy, performance
Related flash tutorials
- OVTH Gateway onboarding — the managed version, no VPS needed
- OpenCode with multiple providers — a client that shines behind the Gateway
- Kiro as an OpenAI endpoint — a standalone reverse-proxy tutorial for the Kiro IDE backend
Self-hosting is the second derivative of control. The first was moving off ChatGPT Pro. This one is moving off the thing you moved onto. Own the endpoint, own the invoice, own the logs.