Not just Claude Code. Any agent.
The broker is a loopback HTTP endpoint — so anything that speaks HTTP can reach your providers through it:
the OpenAI SDKs, LangChain, LlamaIndex, a one-off script, curl, a cron job. Point the client at one
base URL, send one capability header, and the real key never leaves your keychain. STM stops being
"a Claude Code plugin" and becomes a credential broker for every agent you run.
The one rule#
Every recipe on this page is the same two moves. If your client can do both, it can use the broker — no matter the language, framework, or vendor:
- Point it at the broker's base URL instead of the provider's —
http://127.0.0.1:<port>/proxy/<tool>/<label>/<upstream-path>. - Send the loopback capability token as an
x-stm-tokenheader (or a?token=query param when you control the full URL).
That's it. The broker resolves <tool>:<label> from your OS keychain, attaches the real
credential on the outbound call to the provider, and streams the response back with the key scrubbed out.
Wherever the client normally wants an API key, you hand it a throwaway string — the broker drops whatever auth
the client sends and injects the real one itself. See the broker mechanism for exactly how
that boundary is enforced.
The x-stm-token is a capability, not a secret. It authorizes /proxy calls on
loopback only — it can't read your inventory, open the dashboard, or reach off your machine — and it resets when the
daemon restarts. So it is safe in a script, a dotfile, or an editor's settings in a way a real key never is.
Get your base URL + token#
Run stm broker. It ensures the local daemon is up and prints the exact base URL and capability token to drop into any client below:
stm broker
subscribetome broker base URL : http://127.0.0.1:<port>/proxy/<tool>/<label>/<upstream-path> auth : header x-stm-token: <broker-token> Supported targets: anthropic, fal, groq, openai, openrouter, replicate, stripe.
In the snippets below, replace <port> and <broker-token> with the values it
printed. Both are local and ephemeral — the port is chosen at daemon start and the token resets on restart, so
read them from stm broker rather than hard-coding them for good.
curl / any shell#
The baseline. No Authorization header, no key anywhere in the command — only the loopback token:
curl http://127.0.0.1:<port>/proxy/openai/default/v1/chat/completions \ -H "x-stm-token: <broker-token>" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hello"}]}'
When you control the whole URL you can also carry the token as a query param — handy for tools that only take a URL:
append ?token=<broker-token>. The broker strips it before forwarding, so it never reaches the
provider.
Python — OpenAI SDK#
The official openai package accepts a base_url and default_headers. The
api_key is required by the constructor but ignored on the wire — the broker injects the real one:
from openai import OpenAI client = OpenAI( base_url="http://127.0.0.1:<port>/proxy/openai/default/v1", api_key="stm-broker", # ignored — broker injects the real key default_headers={"x-stm-token": "<broker-token>"}, ) resp = client.chat.completions.create( model="gpt-4o-mini", messages=[{"role": "user", "content": "hello"}], ) print(resp.choices[0].message.content)
Python — plain HTTP#
No SDK needed. Any HTTP library works the same way — here requests:
import requests r = requests.post( "http://127.0.0.1:<port>/proxy/openai/default/v1/chat/completions", headers={ "x-stm-token": "<broker-token>", "Content-Type": "application/json", }, json={"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "hello"}]}, ) print(r.json())
Node — OpenAI SDK#
The openai package for JS/TS takes baseURL and defaultHeaders:
import OpenAI from "openai"; const client = new OpenAI({ baseURL: "http://127.0.0.1:<port>/proxy/openai/default/v1", apiKey: "stm-broker", // ignored — broker injects the real key defaultHeaders: { "x-stm-token": "<broker-token>" }, }); const resp = await client.chat.completions.create({ model: "gpt-4o-mini", messages: [{ role: "user", content: "hello" }], }); console.log(resp.choices[0].message.content);
Node — plain fetch#
const resp = await fetch( "http://127.0.0.1:<port>/proxy/openai/default/v1/chat/completions", { method: "POST", headers: { "x-stm-token": "<broker-token>", "content-type": "application/json", }, body: JSON.stringify({ model: "gpt-4o-mini", messages: [{ role: "user", content: "hello" }], }), }, ); console.log(await resp.json());
LangChain & LlamaIndex#
Both frameworks build on the OpenAI client, so they inherit its base_url + default_headers
overrides. Same two moves, same throwaway key.
LangChain
from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="gpt-4o-mini", base_url="http://127.0.0.1:<port>/proxy/openai/default/v1", api_key="stm-broker", default_headers={"x-stm-token": "<broker-token>"}, ) print(llm.invoke("hello").content)
LlamaIndex
from llama_index.llms.openai import OpenAI llm = OpenAI( model="gpt-4o-mini", api_base="http://127.0.0.1:<port>/proxy/openai/default/v1", api_key="stm-broker", default_headers={"x-stm-token": "<broker-token>"}, ) print(llm.complete("hello"))
Anthropic-native clients follow the same pattern — swap the path segment to
/proxy/anthropic/default and point the SDK's base URL there; the broker attaches the
x-api-key for you.
Will my agent work?#
One question decides it: can the client set a custom base URL and a custom request header?
| The client can… | Then… |
|---|---|
| Set base URL + custom header (OpenAI SDKs, LangChain, LlamaIndex, the Vercel AI SDK, most HTTP libraries) | Works directly — use the recipes above. |
| Only lets you build the full request URL (a raw HTTP call, a webhook field) | Works — carry the token as ?token=<broker-token> in the URL. |
| Only lets you set a base URL, no headers (some editor "custom OpenAI endpoint" fields) | Add the header with a tiny loopback forwarder — below. |
Escape hatch: a loopback forwarder for header-less clients
If an agent only exposes a base-URL field, run this ~10-line Bun shim. It listens on a fixed local port, stamps the
x-stm-token header on every request, and forwards to the broker. Point the header-less client at
http://127.0.0.1:8787/proxy/openai/default/v1:
const TOKEN = process.env.STM_BROKER_TOKEN!; // from `stm broker` const UPSTREAM = process.env.STM_BROKER_URL!; // http://127.0.0.1:<port> Bun.serve({ hostname: "127.0.0.1", port: 8787, async fetch(req) { const u = new URL(req.url); const headers = new Headers(req.headers); headers.set("x-stm-token", TOKEN); return fetch(UPSTREAM + u.pathname + u.search, { method: req.method, headers, body: req.body, }); }, });
It never sees a real key either — it only adds the capability token, exactly as the header recipes do. Keep it on loopback; it is a convenience for local tools, not a public proxy.
Both are re-generated when the daemon restarts, so a hard-coded config will break after a reboot. For anything you
run repeatedly, read them from stm broker at start-up (e.g. export STM_BROKER_TOKEN) rather
than pasting a fixed value.
Beyond LLMs#
The broker is not LLM-specific — it forwards any HTTP request to a configured target. The launch set already includes non-model providers, and the same two moves apply. Calling Stripe through the broker, with no secret key in the command:
curl http://127.0.0.1:<port>/proxy/stripe/default/v1/customers?limit=3 \ -H "x-stm-token: <broker-token>"
Whatever the agent, whatever the API, the shape is identical: your keychain holds the key, the broker adds it on the way out, and every brokered call is audited by method, path, and status — never by key. Targets are a data-driven registry, so widening coverage is a single entry.
How the broker works
The /proxy model, the capability token, and the SSRF / redirect / scrub / size-cap invariants that make routing a key through a proxy safe.
The security boundary
What stm defends — keys out of the transcript, off the command — and the endpoint-trust line it does not cross.
Read the security model →