hodor

Unify your external services

Every API behind one stable URL. hodor injects the credentials; your keys never leave your server.

api.openai.com→openai.example.com
api.stripe.com→stripe.example.com
api.linear.app→linear.example.com
How it works

One URL per service

openai.example.com is an integration. Subdomain routing — no path config, nothing to remember.

Credentials handled

Store the key once, encrypted. hodor builds the auth header at request time — callers never see a credential.

Plain HTTP in and out

curl, scripts, agents, browsers. If it speaks HTTPS it works. No SDK, no protocol, no MCP.

Same calls, one key

Four services, four credentials, four leak surfaces — vs the same four calls with one hodor key and nothing else. Same curl, same JSON, same responses; the URL changed and the keys left the room. (hcurl below is a tiny executable wrapping curl with your key; see Example.)

Today — one key per service
linear · lin_api_xxxx
curl -X POST https://api.linear.app/graphql \
  -H "Authorization: lin_api_xxxx" \
  -d '{"query":"{ viewer { name } }"}'
posthog (EU) · phx_xxxx
curl -H "Authorization: Bearer phx_xxxx" \
  https://eu.posthog.com/api/projects/213853/feature_flags/
cloudflare · cfat_xxxx
curl -H "Authorization: Bearer cfat_xxxx" \
  https://api.cloudflare.com/client/v4/user/tokens/verify
axiom (EU) · xa-xxxx
curl -H "Authorization: Bearer xa-xxxx" \
  https://api.axiom.co/v1/datasets
With hodor — $HPT, read-only*
linear.example.com
hcurl linear.example.com/graphql -X POST \
  -d '{"query":"{ viewer { name } }"}'
posthog.example.com
hcurl posthog.example.com/api/projects/213853/feature_flags/
cloudflare.example.com
hcurl cloudflare.example.com/user/tokens/verify
axiom.example.com
hcurl axiom.example.com/v1/datasets

The four upstream credentials stay encrypted on your server — no caller ever holds one. Each caller gets its own key, minted at the finest grain you want (see Keys & permissions). Rotating a service key upstream is one admin call, not a hunt through every tool that ever touched it.

Shell setup

Two files, then hcurl openai.example.com/v1/models | jq. No rc function — those never reach agents, cron, or non-interactive ssh.

FILE 1 · chmod +x

~/.local/bin/hcurl

On PATH (/usr/local/bin for shared boxes).

#!/bin/bash
. ~/.hodor-env
exec curl -sS -H "X-Authorization: Bearer $HPT" "https://$1" "${@:2}"
FILE 2 · chmod 600

~/.hodor-env

Never commit. Point BASH_ENV here for non-interactive shells.

export HPT='<proxy-token>'
export HAT='<admin-token>'
Install

One worker, two secrets, one DNS record. No installer, no lock-in — it's ordinary Cloudflare Workers (or a plain Node/Bun process).

Deploy to Cloudflare

Local dev first:

STEP 1

Clone

git clone git@github.com:butttons/hodor.git && cd hodor
STEP 2

Install

pnpm install
STEP 3

Fill the two keys

cp apps/proxy-worker/.dev.vars.example apps/proxy-worker/.dev.vars
STEP 4

Run

→ localhost:8787

pnpm --filter @hodor/proxy-worker dev

Deploy in four steps:

  1. Fill the two secrets:
    cp apps/proxy-worker/.dev.vars.example apps/proxy-worker/.dev.vars
  2. Create the KV namespace:
    npx wrangler kv namespace create HODOR_KV
  3. Add the DNS record (see DNS & TLS — do not skip):
    proxied A record *.example.com → 192.0.2.0
  4. Deploy — first deploy uploads secrets, later ones are plain:
    npx wrangler deploy --secrets-file .dev.vars (first time only)
    npx wrangler deploy (every time after)

Full steps in llms.txt — including the production-vs-local secret warning (minting against the deployed worker needs the production HODOR_JWT_SECRET, not .dev.vars) and troubleshooting.

Example

Configure once through a small admin API — no code, no redeploys. Four calls, one-time setup (minting is the only open path — everything after needs the key):

STEP 1

Mint an admin key

Knowledge-based: secret must equal HODOR_JWT_SECRET (production, not .dev.vars).

curl -X POST https://example.com/_/keys \
  -H 'content-type: application/json' \
  -d '{"secret":"$HODOR_JWT_SECRET","name":"admin","scopes":["admin"]}'
STEP 2

Store the key

Encrypted at rest; hodor is the only reader.

curl -X PUT -H "X-Authorization: Bearer $HAT" \
  --data-binary 'sk-...' \
  https://example.com/_/admin/secrets/OPENAI_API_KEY
STEP 3

Register the integration

Target, auth header (pick a secret helper), optional probe.

curl -X PATCH https://example.com/_/admin/registry/openai \
  -H "X-Authorization: Bearer $HAT" \
  -H 'content-type: application/json' \
  --data-binary @- <<'JSON'
{
  "id": "openai",
  "url": { "host": "api.openai.com", "path": "/v1" },
  "headers": { "Authorization": "'Bearer ' + kv('OPENAI_API_KEY')" },
  "meta": { "label": "OpenAI", "description": "OpenAI REST API" },
  "probe": { "method": "GET", "path": "/v1/models" }
}
JSON
STEP 4

Call it

Credentials injected for you, upstream. Canonical CLI (see Shell setup above):

hcurl openai.example.com/v1/models
Keys & permissions

One signed JWT per consumer. Restrictions use a single vocabulary — only / except — globally and per integration. except always wins; per-item rules narrow the globals, never widen.

{
  "scopes": ["proxy:call"],
  "integrations": [
    {"id": "stripe"},
    {"id": "razorpay", "only": {"methods": ["GET", "HEAD"]}}
  ]
}

Stripe stays broad, Razorpay is read-only — on the same key. Mint at POST /_/keys, revoke one key without touching the rest, and read every integration in the catalog.

Injecting secrets

Header values are JEXL expressions. Seven helpers, picked per deployment:

Rule: passwords/tokens/keys → secrets; IDs/usernames → vars + variable(). Example: 'Bearer ' + kv('OPENAI_API_KEY'). Reserved helper names can't be shadowed.

DNS & TLS

Do not skip this. The wildcard (*.example.com) is a worker route, and routes do not create DNS — you must add the record by hand: a proxied A record for * pointing at the dummy address 192.0.2.0. Without it the worker deploys fine and serves nothing. The apex (example.com) is the opposite: attach it as a custom domain and Cloudflare provisions DNS + certificate automatically.

Security