FERRULE

The guide

Every screen and command, captured from a running Ferrule with fake providers, and what each one is for. Keys stay on your machine; the endpoint is for the house.

You, at the panel

The control surface at http://localhost:8899 — the simple view for the household, the advanced view for you.

First run

What ferrule serve shows before anything is routed.

The first screen
The first screenA fresh start: nothing routed yet. The card at the top is the address and household key you hand out; the three numbered steps below are the whole setup.
Add a source
Add a sourcePick a provider and paste its key. Ferrule tries the key against the provider before it keeps it — a dead key is never quietly stored.

Your household

The simple view: what to hand out, and what Ferrule found.

Your household
Your householdThe simple view. Sharing is on, the address is the one to give people, and every source Ferrule found or was given is listed with its models.
The household key
The household keyOne key everyone can use, shown on request. Give people their own key only when you want the usage list to say who, or to cut one person off.
What each source serves
What each source servesEach source opens to its models. Local ones are on this machine; cloud ones go to a provider and are counted as such in Usage.
Where your keys live
Where your keys liveThe statement, and the one knob that can contradict it, together: content logging is off, and this is where it would be turned on.

The board

The advanced view: every model, and the sources behind them.

The board
The boardThe advanced view. Every model Ferrule can serve, with where it runs, what it can do, its context length and its cost per million tokens; filter by any of them.
A failure that names itself
A failure that names itselfThe source the demo deliberately misconfigured. The red line is the one clear next action; under it, what the provider said, verbatim.

Aliases and sources

Names apps can ask for, and how a source is added.

Aliases
AliasesA plain name for a ladder of models, tried in order. Apps ask for fast; you decide what fast means, and change it without telling anyone.
Add a source, in full
Add a source, in fullThe full form: provider, a name, the key, an optional test model for accounts whose tier excludes the usual ones, and a base URL for anything OpenAI-compatible.

Usage

What it cost, where it went, what went wrong.

Usage and egress
Usage and egressEvery request, by app and model — count, tokens, latency, cost — and above it the split that no cost dashboard shows: what stayed on this machine and what left.
What went wrong
What went wrongFailed requests with the provider's full words. An app on the network gets a shorter version; the reason stays here, on the machine the key lives on.

Grants and staged changes

Who holds a token; what an agent has proposed.

Grants
GrantsEvery app token, who it was minted for, and when it was last used. Revoke one and the others keep working; the provider key is never the thing an app holds.
Staged by an agent
Staged by an agentA coding agent proposed an alias through the MCP face. Nothing has changed yet — it lands when you apply it here, and a provider key is never in the proposal.

Someone in the house

A person on the wifi with the address and a key. They never see the panel; their app talks to the endpoint.

Using the endpoint

The OpenAI-compatible lane, with a Ferrule token.

$ curl -s http://127.0.0.1:8892/v1/models -H "Authorization: Bearer $FERRULE_KEY" | python3 -m json.tool | head -24
{
    "data": [
        {
            "ferrule": {
                "capabilities": [
                    "chat"
                ],
                "context_length": 8192,
                "source": "ollama",
                "where": "local"
            },
            "id": "gemma3:12b",
            "object": "model",
            "owned_by": "ollama"
        },
        {
            "ferrule": {
                "capabilities": [
                    "chat",
                    "tools"
                ],
                "context_length": 131072,
                "source": "ollama",
                "where": "local"
List the modelsAnything that speaks OpenAI works. The token is a Ferrule key, never a provider key; ferrule in each entry says where the model runs.
$ curl -s http://127.0.0.1:8892/v1/chat/completions \
  -H "Authorization: Bearer $FERRULE_KEY" -H 'Content-Type: application/json' \
  -d '{"model":"fast","messages":[{"role":"user","content":"Say hello in five words."}]}' | python3 -m json.tool
{
    "choices": [
        {
            "finish_reason": "stop",
            "index": 0,
            "message": {
                "content": "ferrule routed this",
                "role": "assistant"
            }
        }
    ],
    "id": "chatcmpl-mock",
    "model": "qwen3:8b",
    "object": "chat.completion",
    "usage": {
        "completion_tokens": 3,
        "prompt_tokens": 11,
        "total_tokens": 14
    }
}
A chat completionAsk for fast and the first reachable rung serves it — here a local model, so the request never left the machine.
$ curl -s -i http://127.0.0.1:8892/v1/models | head -12
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Date: Mon, 21 Sep 2026 16:27:53 GMT
Content-Length: 134

{"error":{"code":401,"message":"missing Authorization header — pass a Ferrule app token as a Bearer token","type":"ferrule_error"}}
Without a tokenNo token, 401. Inference is served to the network only with a valid key; the panel, the vault and the ledger answer only this machine.
$ curl -s http://127.0.0.1:8892/v1/chat/completions \
  -H "Authorization: Bearer $FERRULE_KEY" -H 'Content-Type: application/json' \
  -d '{"model":"groq-typo/llama-3.3-70b-versatile","messages":[{"role":"user","content":"hi"}]}' | python3 -m json.tool
{
    "error": {
        "code": 404,
        "message": "no model, alias, or remap named \"groq-typo/llama-3.3-70b-versatile\": source groq-typo is failed, not live: router: source not live",
        "type": "ferrule_error"
    }
}
When a source is downThe refusal says which source failed and why, so the person can act. The full provider text is on the Usage screen.

You, in the terminal

Every panel operation is also a verb. The CLI reads and writes the same state the daemon serves.

Reading the state

Status, models, aliases, usage.

$ ferrule --help
ferrule — a local key vault and model router

Verbs:
  status   the whole situation in one read (--json for an agent)
  serve    start the daemon (endpoints + control surface)
  open     make sure it is running, then show the panel
  startup  start Ferrule when you log in (on | off)
  add      add a source (paste a key, or adopt a detected runtime)
  ls       list sources / models / aliases
  refresh  re-check a stored source, using the key it already holds
  rm       remove a source, its models, and its key
  alias    create, edit, or remove an alias
  remap    serve a hardcoded model id from a model of your choosing
  key      mint or revoke an app token
  usage    per-app and per-model spend, volume, and egress
  export   write a portable encrypted configuration file
  import   read a portable encrypted configuration file
  version  print the version

Exit codes: 0 the thing worked · 1 it did not, and the reason says why · 2 the
command was wrong.
The verbsEvery command, one line each. Exit codes mean something: 0 it worked, 1 it did not and the reason says why, 2 the command was wrong.
$ ferrule status
ferrule dev · http://127.0.0.1:8892/v1

SOURCE     WHERE  LANE    STATUS  DETAIL
ollama     local  tokens  live    
anthropic  cloud  tokens  live    
deepseek   cloud  tokens  live    
groq       cloud  tokens  live    
groq-typo  cloud  tokens  failed  unreachable · could not reach http://127.0.0.1:1/v1/models: Get "http://127.0.0.1:1/v1/models": dial tcp…

13 model(s) servable — 5 local, 8 cloud
  chat 12 · embeddings 1 · reasoning 1 · tools 10 · vision 5

ALIAS   RUNGS  SERVING
cheap   2      ollama/gemma3:12b
fast    2      ollama/qwen3:8b
smart   2      anthropic/claude-opus-5
vision  2      ollama/llava:13b

3 app token(s), 3 live · last 24h: 78 local, 42 off-machine
vault encrypted file store (age) · catalog 2026-09-03 (stale)

next:
  groq-typo — could not reach http://127.0.0.1:1/v1/models: Get "http://127.0.0.1:1/v1/models": dial tcp 127.0.0.1:1: connect: connection refused  ·  Check the base URL, and that the service is running and listening.
  The capability catalog has not refreshed in over a day. Costs and context lengths may be behind.
The whole situation in one readSources and their state, what is servable, the aliases, tokens, last-day egress, and a next: list of anything that needs you. --json for an agent.
$ ferrule ls models
MODEL                    SOURCE     WHERE  CAPABILITIES          CONTEXT  IN $/M  OUT $/M
claude-haiku-4-5         anthropic  cloud  chat,vision,tools     200000   0.80    4.00
claude-opus-5            anthropic  cloud  chat,vision,tools     200000   15.00   75.00
claude-sonnet-5          anthropic  cloud  chat,vision,tools     200000   3.00    15.00
deepseek-chat            deepseek   cloud  chat,tools            64000    0.27    1.10
deepseek-reasoner        deepseek   cloud  chat,reasoning,tools  64000    0.55    2.19
gemma3:12b               ollama     local  chat                  8192     —       —
llama-3.1-8b-instruct    ollama     local  chat,tools            131072   0.05    0.08
llama-3.3-70b-versatile  groq       cloud  chat,tools            131072   0.59    0.79
llama-4-scout-17b        groq       cloud  chat,vision,tools     131072   0.11    0.34
llava:13b                ollama     local  chat,vision           4096     —       —
mixtral-8x7b-32768       groq       cloud  chat,tools            32768    0.24    0.24
nomic-embed-text         ollama     local  embeddings            8192     —       —
qwen3:8b                 ollama     local  chat,tools            32768    —       —
Every modelThe board, as a table: source, where it runs, capabilities, context, cost.
$ ferrule ls models --local
MODEL                  SOURCE  WHERE  CAPABILITIES  CONTEXT  IN $/M  OUT $/M
gemma3:12b             ollama  local  chat          8192     —       —
llama-3.1-8b-instruct  ollama  local  chat,tools    131072   0.05    0.08
llava:13b              ollama  local  chat,vision   4096     —       —
nomic-embed-text       ollama  local  embeddings    8192     —       —
qwen3:8b               ollama  local  chat,tools    32768    —       —
Only what runs here--local narrows to models on this machine — the ones whose requests never leave it.
$ ferrule alias
ALIAS   LADDER
cheap   ollama/gemma3:12b → deepseek/deepseek-chat
fast    ollama/qwen3:8b → groq/llama-3.3-70b-versatile
smart   anthropic/claude-opus-5 → deepseek/deepseek-reasoner
vision  ollama/llava:13b → anthropic/claude-sonnet-5
The laddersEach alias and its rungs in order. ferrule alias <name> <src>/<model> … sets one.
$ ferrule usage
GROUP                             REQUESTS  ERRORS  TOKENS  LATENCY  COST
zed / claude-opus-5               24        0       336     0ms      $0.0094
research-agent / claude-opus-5    18        0       252     0ms      $0.0070
zed / qwen3:8b                    40        0       560     0ms      $0.0000
transcribe.sh / nomic-embed-text  24        0       336     0ms      $0.0000
research-agent / gemma3:12b       14        0       196     0ms      $0.0000
Total                             120                                $0.0164
Spend by app and modelThe same ledger the panel reads, grouped for a terminal.
$ ferrule usage --egress
WHERE        REQUESTS  BYTES
local        78        24854
off-machine  42        13566

off-machine share  35%
What left the machineRequests and bytes, local against off-machine, and the share.

Minting a key

One token per person or app.

$ ferrule key kitchen-ipad
Minted app token for "kitchen-ipad". This is shown once:

  frl_fKSbN3RRarBseWtNHBx404j8_K4RnMqc

Point the app at http://127.0.0.1:8892/v1 with this as its API key.
Mint a token for one personShown once. The app gets the address and this key; you can revoke it later without touching anyone else's.

A coding agent

The agent face: llms.txt and an MCP manifest served by the daemon. Proposals stage; you apply.

Driving Ferrule from an agent

Read the surface, then propose.

$ curl -s http://127.0.0.1:8892/llms.txt | head -30
# Ferrule

> A local key vault for LLM provider keys, with an OpenAI-compatible router in front of
> it. One Go binary. Keys are encrypted on the user's own disk; nothing is hosted.

Ferrule is a **key vault first, a router second**. A person pastes a provider key once;
Ferrule becomes the single encrypted local home for it. The unified endpoint, the model
board, the alias ladders, and the spend/egress views all fall out of that.

If you are an agent driving Ferrule, read this file and then call `brief`. Do not explore.

## Two things it does that alternatives do not

- **Zero-config discovery.** No config file. It scans localhost for running runtimes
  (Ollama, LM Studio, llama.cpp) and adopts them; a cloud provider is *paste a key*.
- **Egress visibility.** A local, metadata-only ledger of what actually left the machine,
  per app and per model — classified from the address the connection was made to.

## The three faces (they do not overlap)

| Face | Path | Auth |
|---|---|---|
| Inference — raw tokens | `/v1/chat/completions`, `/v1/completions`, `/v1/embeddings`, `/v1/images/generations`, `/v1/models` | Ferrule app token, Bearer |
| Inference — media passthrough | `/p/<source>/…`, provider's native shape, bytes unaltered | Ferrule app token |
| Control | `/api/op/<name>`, `/mcp` | localhost + a per-run control token |

The control face is **not** an inference path. Your own inference goes through the
OpenAI-compatible endpoint like any other client.

## Driving inference
The docs' agent faceThe running daemon serves llms.txt: what Ferrule is and how to drive it, so an agent pointed at a live Ferrule gets the surface without the repo.
$ curl -s http://127.0.0.1:8892/mcp | python3 -c 'import json,sys; m=json.load(sys.stdin); print(m["instructions"]); print(); [print("  "+t["name"].ljust(18), t["description"][:80]) for t in m["tools"]]'
Ferrule's control plane. Read ops answer immediately. Mutating ops are staged and land only when the person applies them. Person-only ops cannot be delegated. Inference does not go through here: point your client at the OpenAI-compatible endpoint with a Ferrule app token.

  add_source         Add a source: paste a key for a known provider, or a key and base URL for an unk
  brief              The whole situation in one bounded read: sources and why any are dark, what the 
  catalog_refresh    Refresh the capability catalog from its maintained source now.
  detect_local       Scan localhost for known runtimes and adopt what is running.
  egress_summary     What left the machine, and what stayed on it.
  export_config      Write a portable encrypted configuration file, sealed under a passphrase so the 
  forget_content     Delete everything the content log holds.
  get_alias          Read one alias and its fallback ladder.
  household_key      Read the key the whole household shares. Person-only: it returns a live credenti
  import_config      Read a portable encrypted configuration file and merge it into local state — sou
  list_aliases       List every alias and its fallback ladder.
  list_grants        List app tokens and what each one spent.
  list_models        List every model on every live source, with capability, context, and cost.
  list_remaps        List every model-id remap.
  list_sources       List every source, live and failed, with its reason.
  list_staged        List control operations staged for a person to apply.
  mint_grant         Mint an app token. Person-only: it returns a credential, shown once.
  read_content       Read the local content log, when content logging is on. Person-only: it returns 
  recent_errors      The requests that failed, newest first, with what the provider actually said.
  refresh_source     Re-probe a source and re-classify its models.
  regenerate_household_key Retire the household key and issue a new one. Every device using the old one sto
  remove_alias       Delete an alias.
  remove_remap       Delete a remap.
  remove_source      Remove a source, its models, and its stored key.
  revoke_grant       Revoke an app token.
  set_alias          Point an alias at an ordered ladder of models.
  set_remap          Serve a hardcoded model id from a model of your choosing.
  set_setting        Change a Ferrule setting.
  set_share_address  Choose which of this machine's addresses the household is given.
  set_startup        Register or unregister the daemon with the login manager. Person-only: it writes
  status             Ferrule's own state: version, config dir, vault backend, catalog date.
  usage_summary      Spend, volume, latency, and errors, grouped as you ask.
The MCP manifestEvery control operation, generated from the same command bus the panel and the CLI use. Read ops answer; mutating ops stage.
$ curl -s -X POST http://127.0.0.1:8892/mcp -H 'Content-Type: application/json' -d '{
  "jsonrpc":"2.0","id":1,"method":"tools/call",
  "params":{"name":"set_alias","arguments":{"name":"everyday","ladder":["ollama/qwen3:8b","deepseek/deepseek-chat"]}}
}' | python3 -c 'import json,sys; print(json.load(sys.stdin)["result"]["content"][0]["text"])'
{
  "args": {
    "ladder": [
      "ollama/qwen3:8b",
      "deepseek/deepseek-chat"
    ],
    "name": "everyday"
  },
  "dropped_undeclared": null,
  "id": "76b5abea04e4",
  "message": "Staged set_alias — apply it in Ferrule to land the change.",
  "op": "set_alias",
  "staged": true,
  "withheld": null
}
A staged proposalThe agent asked for an alias. It is staged, not applied; the person lands it from the panel. A key argument would have been withheld from the payload.

Nothing matches.