Run any agent on any machine — with no API key on the machine.
proxyagent is a secure, self-hosted proxy. It holds your real provider keys in one place; every machine that runs an agent gets only a scoped, revocable pa_ token. The real key never leaves the proxy.
What it is
Every agent harness honours a *_BASE_URL env var. Point that base URL at proxyagent and hand the agent a machine token as its "API key". proxyagent authenticates the token, checks its scope/budget, swaps in the real key, forwards upstream, and logs the call.
remote machine proxy (you host) upstream
┌───────────────┐ pa_ token ┌─────────────────┐ real key ┌──────────┐
│ claude / codex │ ─────────► │ proxyagent serve │ ───────► │ Anthropic│
│ (no real key) │ ◄───────── │ scope·budget·log │ ◄─────── │ OpenAI │
└───────────────┘ stream └─────────────────┘ └──────────┘
1 · Install
pip install proxyagent
# or, isolated as a CLI tool:
uv tool install proxyagent
2 · Run the proxy
Run this on a box you control — it's the only place real keys live. It prints an admin token and serves this dashboard.
proxyagent serve
# → Admin token: pa_admin_… · Dashboard: __ORIGIN__
proxyagent admin-token. Paste it into the dashboard login to manage keys, tokens, routing and activity.3 · Add a real provider key
Three ways — pick one. The key is stored encrypted (set PROXYAGENT_SECRET_KEY) or read from the environment; it's never returned or logged.
# a) environment (simplest):
export ANTHROPIC_API_KEY=sk-ant-…
export OPENAI_API_KEY=sk-…
# b) CLI (stored, encrypted):
proxyagent provider add anthropic --key sk-ant-…
proxyagent provider add openai --key sk-…
# c) the dashboard → Access keys → + Create access key (API key / OAuth / Bedrock / Azure / Vertex)
A provider is a pool: add several keys (and auth types) and proxyagent rotates + fails over across them on 429/5xx.
4 · Mint a machine token
This is what the remote machine holds. Scope it, give it a TTL/budget, revoke it anytime.
proxyagent token new macbook-01 --scope "anthropic:claude-*" --budget 5.00
# → pa_… (shown once)
Or mint from the dashboard's Machine tokens tab (it shows a ready-to-run curl and a copy-.env snippet).
5 · Run an agent — token + prompt, that's it
This is the whole point: hand the SDK a token and a prompt, and it launches a real coding agent (the actual Claude Code / Codex CLI) on this machine against that prompt — with no API key here. Every model call the agent makes routes through the proxy, which holds the real key.
# pip install proxyagent — then:
import proxyagent
proxyagent.run("build a SwiftUI todo app and run the tests",
token="pa_…", proxy="__ORIGIN__")
# → launches Claude Code here, keyless. harness="codex" for Codex,
# command="my-agent {goal}" for any custom agent.
Supported agents (install the CLI you want)
npm i -g @anthropic-ai/claude-code # Claude Code → harness="claude-code" (default)
npm i -g @openai/codex # Codex → harness="codex" (or: brew install codex)
proxyagent drives the real CLI locally and wires it to the proxy
for you — Claude Code via ANTHROPIC_BASE_URL; Codex via a one-off model provider (it ignores
OPENAI_BASE_URL). Both are verified end-to-end: every call lands on the proxy, keyless.
From the CLI
PROXYAGENT_TOKEN=pa_… proxyagent run codex \
--goal "fix the failing tests" --proxy __ORIGIN__
PROXYAGENT_TOKEN=pa_… proxyagent run claude-code --goal "ship it" --proxy __ORIGIN__
Claude Code by hand (just env)
export ANTHROPIC_BASE_URL=__ORIGIN__/anthropic
export ANTHROPIC_API_KEY=pa_… # the machine token, NOT the real key
claude -p "ship it"
# (Codex needs a model-provider override — just use `proxyagent run codex`.)
SDK — manage the proxy programmatically
Mint tokens, manage credentials, and host the proxy from Python:
import proxyagent
app = proxyagent.create_app() # ASGI app — embed in your service
admin = proxyagent.Admin("__ORIGIN__", "pa_admin_…")
tok = admin.mint("laptop", scope=["anthropic:claude-*"], ttl_seconds=3600)
# …then run an agent with that token (the headline call):
proxyagent.run("build the app", token=tok, proxy="__ORIGIN__")
Raw HTTP / curl
curl __ORIGIN__/anthropic/v1/messages \
-H "x-api-key: pa_…" \
-d '{"model":"claude-3-5-haiku","max_tokens":100,
"messages":[{"role":"user","content":"hello"}]}'
OpenAI-shaped providers use /<provider>/v1/chat/completions; Anthropic uses /anthropic/v1/messages. List routable models at /v1/models.
Offline mode (no keys, no spend)
Map every model to the built-in mock and the whole pipeline (auth, scope, budget, logging) runs end-to-end with zero keys — perfect for local dev, demos and CI.
proxyagent alias set '*' mock
curl __ORIGIN__/anthropic/v1/messages -H "x-api-key: pa_…" \
-d '{"model":"mock","max_tokens":50,"messages":[{"role":"user","content":"hi"}]}'
Proxied tools & the agentic loop
proxyagent can hold your tool keys too. Send x-proxyagent-tools: on and it injects tool defs, then (non-streaming) runs the whole loop server-side: the model asks for a tool → proxyagent executes it (its key stays here) → feeds the result back → repeats until a final answer.
export TAVILY_API_KEY=tvly-… # web_search uses this; agents never see it
curl __ORIGIN__/anthropic/v1/messages \
-H "x-api-key: pa_…" -H "x-proxyagent-tools: on" \
-d '{"model":"claude-3-5-haiku","max_tokens":300,
"messages":[{"role":"user","content":"what happened today?"}]}'
# response header x-proxyagent-tool-steps: N · cap with x-proxyagent-tool-steps-max
Budgets, limits & security
proxyagent token new ci --budget 5.00 # per-token $ cap → 402 when hit
proxyagent token new ci --scope "anthropic:claude-*" --ttl 3600 --rate 60
export PROXYAGENT_PROVIDER_BUDGETS='{"anthropic":200}' # per-provider $ ceiling
export PROXYAGENT_PROVIDER_RATE_LIMITS='{"openai":1000}'
export PROXYAGENT_BUDGET_WEBHOOK=https://hooks.you.com/… # alert on cap crossing
Tokens are stored hashed, scoped (provider:model globs), expiring, revocable, rate-limited, and optionally IP-locked (allowed_ips CIDRs). Real keys are read from env / stored encrypted and never logged.
Deploy
docker compose up -d # proxy at :8080; admin token in logs
docker compose --profile postgres up -d # shared Postgres backend
# point at Postgres directly:
export PROXYAGENT_DATABASE_URL=postgresql://user:pass@host/db
Health: GET /healthz (liveness) · GET /readyz (DB-ping readiness, 503 if down). Metrics: GET /metrics (Prometheus, incl. a latency histogram).
Endpoint reference
| Endpoint | Auth | What |
|---|---|---|
POST /<provider>/v1/chat/completions | machine token | OpenAI-shaped proxy |
POST /<provider>/v1/messages | machine token | Anthropic-shaped proxy |
GET /v1/models | machine token | routable model catalog |
GET /healthz · /readyz | none | liveness / readiness |
GET /metrics | admin* | Prometheus metrics |
/admin/tokens · /admin/providers | admin | manage tokens / credentials |
GET /admin/summary | admin | Markdown status report |
Environment variables
| Var | Purpose |
|---|---|
ANTHROPIC_API_KEY, OPENAI_API_KEY, … | real upstream keys (env source) |
PROXYAGENT_ADMIN_TOKEN | fixed admin token (else auto-generated) |
PROXYAGENT_SECRET_KEY | Fernet key → encrypt stored creds at rest |
PROXYAGENT_DATABASE_URL | Postgres DSN (default: local SQLite) |
PROXYAGENT_CACHE_TTL | response cache TTL seconds (0 = off) |
PROXYAGENT_PROVIDER_BUDGETS / _RATE_LIMITS | per-provider $ ceiling / req-per-min |
PROXYAGENT_MAX_BODY_BYTES | reject larger request bodies (413) |
PROXYAGENT_CORS_ORIGINS | allow browser clients (CORS) |
PROXYAGENT_BUDGET_WEBHOOK / _EVENT_WEBHOOK / _WEBHOOK_SECRET | alerts + lifecycle events (HMAC-signed) |
PROXYAGENT_LOG_RETENTION_DAYS | auto-trim the audit log on boot |
proxyagent · Apache-2.0 · github.com/teddyoweh/proxyagent