ultrarouter docs

Get started

Connect your agent, wire a trusted outcome signal, turn on learning, then watch, pause or roll back. Or copy one prompt into Claude Code or Codex and let it do the setup.

Preview: evidence status

Plug-and-play learning is a preview. The whole loop runs on the local stack today: outcome receipts, learned candidates evaluated against the version you run, and pause, disable and rollback. Whether learning improves a given agent is not yet demonstrated. Read the evidence status before you rely on it.

ultrarouter sits between your agent and its models. You tell it whether each finished task succeeded, from a source you trust. It turns those outcomes into candidate adaptations, evaluates each candidate against the version you run today, deploys it only when it wins, and keeps learning from the new outcomes. That cycle is what "recursive" means here: outcome, adaptation, evaluation, deployment, new outcome. It changes the context ultrarouter serves your agent, never the model's weights.

  1. Outcome

    A source you trust says whether a finished task succeeded.

  2. Adaptation

    ultrarouter drafts a change to the context it serves your agent, from that evidence.

  3. Evaluation

    The candidate runs against the current version on live tasks, under a rule fixed in advance.

  4. Deployment

    Only a candidate that wins becomes active. Otherwise the current version stays.

  5. New outcome

    The adapted agent's results become the evidence for the next round.

The loop repeats: each new outcome starts the next round. It changes the context ultrarouter serves your agent, never the model's weights.

The fast path: hand it to your coding agent

Pick your coding agent, your language and how you want to connect, then copy the prompt. It tells the agent to inspect your project, reuse its configuration, install the SDK from a local build, wire a trusted outcome, run one verified episode and confirm its receipt. It asks you only for sign-in, consent and keys, and it never contains a key.

Set it up with your coding agent

Copy the prompt into Claude Code or Codex. It walks your agent through the whole setup and asks you only for sign-in, consent and keys. It never contains a key.

Coding agent
Project language
How to connect

Prompt for Claude Code: Add learning to an existing app

Recipe existing-app, revision 4, TypeScript

Set up ultrarouter learning in this project. You are Claude Code.

ultrarouter is an OpenAI-compatible gateway. The project keeps its agent; it reports a trusted outcome for each finished task, and ultrarouter turns those outcomes into candidate adaptations, evaluates each one against the version running today, deploys it only if it wins, and keeps learning from the new outcomes. Learning changes the context ultrarouter serves the agent, not the model's weights.

This setup
- Recipe: Add learning to an existing app (existing-app, revision 4, schema ur.setup-recipes/1). Machine-readable copy: http://localhost:3002/setup-recipes.json
- Project language: TypeScript. If the project keeps agent instructions in CLAUDE.md, add a short note there on where the gateway is configured and where outcomes are reported.
- Learning is not on the hosted service yet (the hosted gateway at https://api.ultrarouter.io is invite-only and serves routing and compression), so this setup uses the local stack: the gateway at http://localhost:8080 and the dashboard at http://localhost:3000.
- Learning is a preview. The loop runs on the local stack; if an outcome-receipt or learning endpoint returns 404, the checkout is older than it, so tell me and stop at that step. A working setup proves the plumbing, not that learning helps: http://localhost:3002/docs/evidence-status.md

Rules
- Never write a key into source files, tests, fixtures, logs, commit messages or your replies. Read it from UR_API_KEY in the environment. Before adding it to a .env file, confirm .env is git-ignored, and never print that file.
- Ask me only for what needs my identity, my consent or credentials you cannot get: signing up or in, email verification, two-factor enrolment, accepting terms, the workspace Learning opt-in (corrections and knowledge consent), provider keys, minting the ultrarouter API key, the path of my ultrarouter checkout, and a yes before you turn learning on or use its controls. Do everything else yourself. Never create an account, accept terms or change consent for me.
- Claude Code may ask me before it runs a shell command. When you request one that calls the gateway, say why it is needed.
- Reuse the project's existing configuration: read UR_BASE_URL and UR_API_KEY where the project already reads its settings, and keep its client, its retry and timeout settings, and its tests.
- Never claim that learning improved anything. Report what the receipt and the learning status say.

Steps
1. Inspect the project. Identify its language and package manager, the existing LLM client (the openai package, another OpenAI-compatible SDK, or raw HTTP calls) and its tests. Find where the client's base URL, key and model are configured, and how a finished task is judged today: that judgment is the outcome signal to reuse.
2. Read the current ultrarouter docs before you change anything:
   - Get started: http://localhost:3002/docs/get-started.md
   - Record outcomes: http://localhost:3002/docs/guides/outcomes.md
   - Evidence status: http://localhost:3002/docs/evidence-status.md
   - Set up with Claude Code or Codex: http://localhost:3002/docs/guides/agent-integrations.md
   - Setup recipes (JSON): http://localhost:3002/setup-recipes.json
3. Check that the local gateway answers.
   Learning is not on the hosted service yet (https://api.ultrarouter.io is invite-only and serves routing and compression), so this setup runs on the local stack from the ultrarouter repository: the gateway on http://localhost:8080 and the dashboard on http://localhost:3000. If the readiness check does not answer, the stack has to be started in the ultrarouter checkout, which the coding agent may not have access to.
   Commands:
     curl -sS http://localhost:8080/readyz
     pnpm dev:e2e   (in the ultrarouter checkout)
   Expected output (illustrative; ids differ):
     {"status":"ready","service":"gateway","checks":{…}}
4. Create or sign in to the account.
   Sign up at http://localhost:3000/signup (or sign in at http://localhost:3000/login) and create the workspace. Email is not delivered on the local stack: the verification link is in the test outbox at http://localhost:8080/auth/test/outbox?to=<email>. Enrol two-factor authentication when asked; an owner needs it to mint keys and to change learning. As the workspace owner, choose Allow learning on the dashboard Learning page for the call type, or in the Learn onboarding flow. This workspace opt-in grants both corrections consent (store task content) and knowledge consent (use what it learns); corrections consent alone is insufficient. The equivalent owner-session request is PUT /v1/workspaces/<workspace id>/self-learning with {"enabled":true}; a bearer API key cannot grant these permissions. Under Providers & BYOK, add a key (Add key) for the provider whose models the agent and the learning reflection will use, or add a custom provider with its own key: learning only ever uses a model the workspace owns, never a platform key. A new workspace starts on prepaid UR credits, which never pay the workspace's own keys, so choose BYOK funding once from the signed-in session: POST /v1/workspaces/<workspace id>/billing/funding with {"mode":"byok","allowPlatformFallback":false,"requestKey":"<16 to 128 letters, digits, - or _>"}. Until then a request only your own key can serve is refused with HTTP 403 permission. Then mint an API key in Settings, API keys, with the gateway:invoke, outcomes:write and config:read scopes; add config:write only if the agent should turn learning on, pause it or roll it back through the API.
   This step is mine: ask me to do it, then wait until I confirm it is done.
   Docs: http://localhost:3000/signup | http://localhost:3000/login | Provider keys: http://localhost:3002/docs/guides/providers-byok.md | Security and data handling: http://localhost:3002/docs/security.md
5. Put the endpoint and the key in the environment.
   read -rs reads the key without echoing it, so it stays out of the terminal and the shell history. Claude Code passes the environment it was started from to its shell. Export UR_BASE_URL and UR_API_KEY before starting it; if it is already running, restart it with the resume command so this conversation continues. If the project loads a .env file, add UR_API_KEY there only after confirming .env is git-ignored. To use the management MCP, export ULTRAROUTER_API_KEY (a separate key) the same way before starting the agent. The last command checks the key is visible without printing it.
   This step is mine: ask me to do it, then wait until I confirm it is done.
   Commands:
     export UR_BASE_URL=http://localhost:8080
     read -rs UR_API_KEY && export UR_API_KEY
     claude --continue
     test -n "$UR_API_KEY" && echo "UR_API_KEY is set"
   Expected output (illustrative; ids differ):
     UR_API_KEY is set
   Docs: Set up with Claude Code or Codex: http://localhost:3002/docs/guides/agent-integrations.md
6. Install the TypeScript SDK from a local build (optional).
   Only when the project has no LLM client yet, or I ask for the native SDK: the existing OpenAI-compatible client is enough for the connection. @ultrarouter/sdk is not on the npm registry, so never install it from there. Build it from the feat/learning-loop branch of the public source repository, cloned next to this project. Install the tarball with the project's package manager (pnpm add and yarn add take the same path). Version 0.1.0 includes the learning and outcome-receipt helpers (see the SDK guide); the commands in this recipe use the same REST calls, so they also work without them.
   Commands:
     git clone --branch feat/learning-loop --single-branch https://github.com/lucastonon/ultrarouter-sdk-typescript ../ultrarouter-sdk-typescript
     npm ci && npm run build && npm pack   (in ../ultrarouter-sdk-typescript)
     npm install ../ultrarouter-sdk-typescript/ultrarouter-sdk-0.1.0.tgz
   Expected output (illustrative; ids differ):
     ultrarouter-sdk-0.1.0.tgz   (the last line npm pack prints)
   Docs: https://github.com/lucastonon/ultrarouter-sdk-typescript | TypeScript SDK: http://localhost:3002/docs/guides/sdk-typescript.md
7. Point the existing client at the gateway.
   Keep the project's OpenAI-compatible client and change only its configuration: the base URL becomes "$UR_BASE_URL/v1", the key comes from UR_API_KEY, and an X-UR-Call-Type header names this agent, because learning is per call type. If one task makes several model calls, send the same X-UR-Artifact header (the task or run id) on each of them. Keep the x-ur-request-id response header of the task's last call: the outcome is reported on it. With the openai package, .withResponse() exposes the headers.
   your existing client module:
   ```ts
   import OpenAI from "openai";

   // Only the configuration changes: base URL, key and the call-type header.
   const client = new OpenAI({
     baseURL: `${process.env.UR_BASE_URL}/v1`,
     apiKey: process.env.UR_API_KEY,
     defaultHeaders: { "X-UR-Call-Type": "my-agent" },
   });

   // Every model call of one task carries the same task id; keep the last request id.
   const { data: completion, response } = await client.chat.completions
     .create({ model, messages }, { headers: { "X-UR-Artifact": taskId } })
     .withResponse();
   const lastRequestId = response.headers.get("x-ur-request-id");

   // When the task is finished, report your verifier's verdict once.
   await fetch(`${process.env.UR_BASE_URL}/v1/requests/${lastRequestId}/outcomes`, {
     method: "POST",
     headers: {
       Authorization: `Bearer ${process.env.UR_API_KEY}`,
       "Content-Type": "application/json",
     },
     body: JSON.stringify({
       channel: "computable",
       kind: passed ? "oracle_pass" : "oracle_fail",
       source_event_id: taskId,
     }),
   });
   ```
   Docs: Quickstart: http://localhost:3002/docs/quickstart.md
8. Report a trusted outcome for each finished task.
   Use the most trustworthy signal the project already has that a finished task succeeded, and report it once per task on the task's last request id. A verifier (tests, a schema or exact-match check) or a business rule sends channel computable with kind oracle_pass or oracle_fail. A delayed webhook sends channel resolution with kind resolved_true or resolved_false. A human review sends channel correction with kind accepted_unchanged or rejected. Self-rating (the agent or a model grading its own answer, or a thumbs-up) is weaker: it is recorded as a proxy and never teaches. An HTTP 200 or a tool call that returned is not task success. If the outcome is unknown, send nothing. A retry reuses the same source_event_id, so it never counts twice.
   Commands:
     curl -sS -X POST "$UR_BASE_URL/v1/requests/$REQUEST_ID/outcomes" -H "Authorization: Bearer $UR_API_KEY" -H "Content-Type: application/json" -d '{"channel":"computable","kind":"oracle_pass","source_event_id":"<task id>"}'
   Expected output (illustrative; ids differ):
     HTTP 202 with "status" accepted, duplicate or pending, and a "receipt" (ocr_…) that stays pending until the gateway has recorded the request, then becomes applied.
   Docs: Record outcomes: http://localhost:3002/docs/guides/outcomes.md
9. Install the ultrarouter MCP servers (optional).
   Neither MCP server is on a package registry yet. In the ultrarouter checkout, build and pack the management MCP and install that tarball, build the documentation MCP, then let the installer register both for this agent; it keeps every other host setting and never writes a key. The management server reads ULTRAROUTER_API_KEY (a separate key with config:read; config:write only if the agent may pause, disable or roll back learning) and ULTRAROUTER_BASE_URL from the environment the agent was started in. It reads learning status, evaluations and outcome receipts and can pause, disable or roll back; it cannot report an outcome, so it can never mark a task as succeeded, and it does not route this agent's own traffic through ultrarouter.
   Ask me before you run it.
   Commands:
     pnpm --filter @ultrarouter/mcp build   (in the ultrarouter checkout)
     (cd packages/mcp && pnpm pack && npm install -g ./ultrarouter-mcp-0.1.0.tgz)   (in the ultrarouter checkout)
     pnpm --filter "@ultrarouter/docs-mcp..." build   (in the ultrarouter checkout)
     ultrarouter-mcp install --host claude --with-docs --docs-repo "$PWD"   (in the ultrarouter checkout)
     claude mcp get ultrarouter
     ultrarouter-mcp check
   Expected output (illustrative; ids differ):
     Connected to ultrarouter at http://localhost:8080.
     Workspace id: …   (from ultrarouter-mcp check)
   Docs: MCP servers for Claude Code and Codex: http://localhost:3002/docs/guides/mcp-servers.md
10. Run one verified episode.
   Add or extend a test or script that runs one real task end to end through the gateway, applies the real verifier and reports the outcome. Run it with the project's own test command and keep its request id.
11. Confirm the outcome receipt and the learning state.
   The outcome's receipt (ocr_…) must reach applied. Pending is normal until the gateway has recorded the request; rejected, failed or expired carries a reason to fix. The learning routes take the call type's id (ct_…), not its slug: the gateway created the call type on the first request that named it, so look the id up by its slug my-agent (with jq, or read it from the JSON) and use it as UR_CALL_TYPE_ID below. Then read the learning state and report it as returned; it is off until learning is turned on.
   Commands:
     curl -sS "$UR_BASE_URL/v1/requests/$REQUEST_ID/outcome-receipts" -H "Authorization: Bearer $UR_API_KEY"
     curl -sS "$UR_BASE_URL/v1/call-types" -H "Authorization: Bearer $UR_API_KEY" | jq -r '.data[] | select(.slug == "my-agent") | .id'
     curl -sS "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning" -H "Authorization: Bearer $UR_API_KEY"
   Expected output (illustrative; ids differ):
     {"object":"list","data":[{"object":"outcome_receipt","id":"ocr_…","status":"applied",…}]}
     ct_…
     {"object":"learning","call_type_id":"ct_…","state":"off",…}
   Docs: Evidence status: http://localhost:3002/docs/evidence-status.md
12. Turn on learning for the call type.
   Learning stays off until it is turned on for the call type, by its id. Three things must hold first, and the route names the one that is missing. Both corrections consent and knowledge consent are on through the workspace Allow learning opt-in (409 corrections_consent_required if either is missing; the owner uses the dashboard Learning page or PUT /v1/workspaces/<workspace id>/self-learning with {"enabled":true} from a signed-in owner session, never a bearer API key). reflection_model is a model the workspace owns, through a provider key it stored or one of its custom providers (400 reflection_model_not_workspace_owned with the reason; there is no platform or paid fallback, and a workspace on prepaid billing must fund with its own keys, byok). The caller is an owner or admin, or a key with config:write. With promotion mode "evaluate", the default, a learned candidate is evaluated before replacing the active version. During the comparison it serves only its randomly assigned share of eligible episodes; negative or inconclusive results keep the current active version. Afterwards, reflection.provider in the learning status says whether the reflection model is usable and, if not, why.
   Ask me before you run it.
   Commands:
     curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/enable" -H "Authorization: Bearer $UR_API_KEY" -H "Content-Type: application/json" -d '{"reflection_model":"<a model the workspace owns>","promotion":{"mode":"evaluate"}}'
   Expected output (illustrative; ids differ):
     {"object":"learning_control","action":"enable","changed":true,…,"learning":{"state":"collecting",…}}
   Docs: Provider keys: http://localhost:3002/docs/guides/providers-byok.md
13. Pause, disable or roll back.
   These are the controls for later and the core of the rollback instructions; do not run them during setup. Pause stops reflecting and evaluating but keeps serving the active version; resume undoes it. Disable stops serving learned lessons and stops a running evaluation. Rollback points the call type at an earlier version from the version history (version_id, epb_…; omitted, the version that was active before the current one). Every control is audited and needs an owner or admin, or a key with config:write.
   Ask me before you run it.
   Commands:
     curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/pause" -H "Authorization: Bearer $UR_API_KEY"
     curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/resume" -H "Authorization: Bearer $UR_API_KEY"
     curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/disable" -H "Authorization: Bearer $UR_API_KEY"
     curl -sS "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/versions" -H "Authorization: Bearer $UR_API_KEY"
     curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/rollback" -H "Authorization: Bearer $UR_API_KEY" -H "Content-Type: application/json" -d '{"version_id":"<an earlier version id>"}'
14. Summarize. List every file you changed and why, the commands you ran with their results, the request id, the receipt status and the learning state. Then give exact rollback instructions: revert those files, point the client back at its previous base URL, disable learning or roll back to an earlier version with the calls above, remove the MCP servers with ultrarouter-mcp uninstall --host claude --with-docs (or the plugin with claude plugin uninstall ultrarouter@ultrarouter-local) if you added them, and remind me to revoke the API keys in the dashboard Settings.

The change to your existing client

The lines to change in an existing project. model, messages, taskId and passed come from your own code; passed is your verifier, not a model grading itself.

import OpenAI from "openai";

// Only the configuration changes: base URL, key and the call-type header.
const client = new OpenAI({
  baseURL: `${process.env.UR_BASE_URL}/v1`,
  apiKey: process.env.UR_API_KEY,
  defaultHeaders: { "X-UR-Call-Type": "my-agent" },
});

// Every model call of one task carries the same task id; keep the last request id.
const { data: completion, response } = await client.chat.completions
  .create({ model, messages }, { headers: { "X-UR-Artifact": taskId } })
  .withResponse();
const lastRequestId = response.headers.get("x-ur-request-id");

// When the task is finished, report your verifier's verdict once.
await fetch(`${process.env.UR_BASE_URL}/v1/requests/${lastRequestId}/outcomes`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.UR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    channel: "computable",
    kind: passed ? "oracle_pass" : "oracle_fail",
    source_event_id: taskId,
  }),
});
Every step the prompt walks through
  1. Check that the local gateway answers
    • curl -sS http://localhost:8080/readyz
    • pnpm dev:e2e
  2. Create or sign in to the account (you do this one)
  3. Put the endpoint and the key in the environment (you do this one)
    • export UR_BASE_URL=http://localhost:8080
    • read -rs UR_API_KEY && export UR_API_KEY
    • claude --continue
    • test -n "$UR_API_KEY" && echo "UR_API_KEY is set"
  4. Install the TypeScript SDK from a local build (optional)
    • git clone --branch feat/learning-loop --single-branch https://github.com/lucastonon/ultrarouter-sdk-typescript ../ultrarouter-sdk-typescript
    • npm ci && npm run build && npm pack
    • npm install ../ultrarouter-sdk-typescript/ultrarouter-sdk-0.1.0.tgz
  5. Point the existing client at the gateway
  6. Report a trusted outcome for each finished task
    • curl -sS -X POST "$UR_BASE_URL/v1/requests/$REQUEST_ID/outcomes" -H "Authorization: Bearer $UR_API_KEY" -H "Content-Type: application/json" -d '{"channel":"computable","kind":"oracle_pass","source_event_id":"<task id>"}'
  7. Install the ultrarouter MCP servers (optional)
    • pnpm --filter @ultrarouter/mcp build
    • (cd packages/mcp && pnpm pack && npm install -g ./ultrarouter-mcp-0.1.0.tgz)
    • pnpm --filter "@ultrarouter/docs-mcp..." build
    • ultrarouter-mcp install --host claude --with-docs --docs-repo "$PWD"
    • claude mcp get ultrarouter
    • ultrarouter-mcp check
  8. Run one verified episode
  9. Confirm the outcome receipt and the learning state
    • curl -sS "$UR_BASE_URL/v1/requests/$REQUEST_ID/outcome-receipts" -H "Authorization: Bearer $UR_API_KEY"
    • curl -sS "$UR_BASE_URL/v1/call-types" -H "Authorization: Bearer $UR_API_KEY" | jq -r '.data[] | select(.slug == "my-agent") | .id'
    • curl -sS "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning" -H "Authorization: Bearer $UR_API_KEY"
  10. Turn on learning for the call type
    • curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/enable" -H "Authorization: Bearer $UR_API_KEY" -H "Content-Type: application/json" -d '{"reflection_model":"<a model the workspace owns>","promotion":{"mode":"evaluate"}}'
  11. Pause, disable or roll back
    • curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/pause" -H "Authorization: Bearer $UR_API_KEY"
    • curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/resume" -H "Authorization: Bearer $UR_API_KEY"
    • curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/disable" -H "Authorization: Bearer $UR_API_KEY"
    • curl -sS "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/versions" -H "Authorization: Bearer $UR_API_KEY"
    • curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/rollback" -H "Authorization: Bearer $UR_API_KEY" -H "Content-Type: application/json" -d '{"version_id":"<an earlier version id>"}'

The prompts are generated from versioned setup recipes. Agents can read every recipe, with exact commands, environment variable names and expected output, at /setup-recipes.json (schema ur.setup-recipes/1). The differences between Claude Code and Codex are on Set up with Claude Code or Codex, and the MCP servers and plugins on MCP servers for Claude Code and Codex.

Before you start

  • The local stack. Learning is not on the hosted service yet (the hosted gateway at https://api.ultrarouter.io is invite-only and serves routing and compression), so this guide runs on the local stack. In your checkout of the ultrarouter repository, pnpm dev:e2e starts the gateway on http://localhost:8080 and the dashboard on http://localhost:3000. curl -sS http://localhost:8080/readyz answers "status":"ready" when it is up.
  • Your account. Sign up at http://localhost:3000/signup and create a workspace. Email is not delivered on the local stack: the verification link is in the test outbox at http://localhost:8080/auth/test/outbox?to=<email>. Enrol two-factor authentication when asked; an owner needs it to mint keys and to change learning.
  • Workspace Learning opt-in. As the owner, choose Allow learning on the call type's dashboard Learning page or in the Learn onboarding flow. It grants both corrections consent to store the task and the agent's steps and knowledge consent to use what it learns; corrections consent alone is insufficient. The equivalent request is PUT /v1/workspaces/{id}/self-learning with {"enabled":true} from a signed-in owner session, not a bearer API key. Revoking corrections consent stops learning and deletes what was stored.
  • A model your workspace owns. Under Providers & BYOK, choose Add key for the provider whose models your agent and the learning reflection will use, or add a custom provider with its own key (provider keys). Learning only ever calls a model the workspace owns, never a platform key, and there is no paid fallback.
  • BYOK funding. A new workspace starts on prepaid UR credits, which never pay your own keys. Choose BYOK funding once from your signed-in session with POST /v1/workspaces/<workspace id>/billing/funding and {"mode":"byok","allowPlatformFallback":false,"requestKey":"<16 to 128 letters, digits, - or _>"}. Until then a request only your own key can serve is refused with HTTP 403 permission.
  • An API key. In Settings, under API keys, mint a key with the gateway:invoke, outcomes:write and config:read scopes. Add config:write only if a script or your coding agent should turn learning on, pause it or roll it back.

Put the gateway URL and the key in the environment of the shell that runs your app and your coding agent, and never in source control. read -rs keeps the key out of your terminal and shell history:

export UR_BASE_URL="http://localhost:8080"
read -rs UR_API_KEY && export UR_API_KEY

Start your coding agent after this, so it inherits both. Codex removes variables whose names contain KEY, SECRET or TOKEN from the commands it runs, so start it with this override (it lets every such variable through, so start it from a shell that holds only the secrets this project needs):

codex -c shell_environment_policy.ignore_default_excludes=true

Connect your agent

Learning is per call type, so name your agent's traffic: send an X-UR-Call-Type header (or a call_type field) on every request. The gateway creates the call type the first time a request names it. If one task takes several model calls, send the same X-UR-Artifact header (your task or run id) on each, so they form one episode. Every response carries an x-ur-request-id header; keep the one from the task's last call, because the outcome is reported on it.

With your existing OpenAI client, change only its configuration:

import OpenAI from "openai";

const client = new OpenAI({
  baseURL: `${process.env.UR_BASE_URL}/v1`,
  apiKey: process.env.UR_API_KEY,
  defaultHeaders: { "X-UR-Call-Type": "my-agent" },
});

// Every model call of one task carries the same task id; keep the last request id.
const { data: completion, response } = await client.chat.completions
  .create({ model, messages }, { headers: { "X-UR-Artifact": taskId } })
  .withResponse();
const lastRequestId = response.headers.get("x-ur-request-id");

With your existing OpenAI client, change only its configuration:

import os
from openai import OpenAI

client = OpenAI(
    base_url=f"{os.environ['UR_BASE_URL']}/v1",
    api_key=os.environ["UR_API_KEY"],
    default_headers={"X-UR-Call-Type": "my-agent"},
)

# Every model call of one task carries the same task id; keep the last request id.
raw = client.chat.completions.with_raw_response.create(
    model=model, messages=messages, extra_headers={"X-UR-Artifact": task_id}
)
last_request_id = raw.headers.get("x-ur-request-id")
completion = raw.parse()

Neither SDK is on a package registry. Clone the public feat/learning-loop branch to get the learning preview (0.1.0), build it, and install the resulting tarball or wheel. The default main branch does not carry this learning preview. Exact file names are in /setup-recipes.json.

# TypeScript: clone the learning branch and build its tarball, then install the file
git clone --branch feat/learning-loop --single-branch https://github.com/lucastonon/ultrarouter-sdk-typescript ../ultrarouter-sdk-typescript
(cd ../ultrarouter-sdk-typescript && npm ci && npm run build && npm pack)
npm install ../ultrarouter-sdk-typescript/ultrarouter-sdk-0.1.0.tgz

# Python: clone the learning branch and build its wheel, then install the file
git clone --branch feat/learning-loop --single-branch https://github.com/lucastonon/ultrarouter-sdk-python ../ultrarouter-sdk-python
(cd ../ultrarouter-sdk-python && uv build)
pip install ../ultrarouter-sdk-python/dist/ultrarouter-0.1.0-py3-none-any.whl

The SDK reads UR_BASE_URL and UR_API_KEY and takes call_type on every request. See the TypeScript SDK and Python SDK guides. The copy-to-agent prompt above has a complete runnable example for each language.

Wire a trusted outcome signal

When a task is finished, report once whether it succeeded, on the task's last request id. The source matters more than anything else you set up:

Your sourceWhat to send (channel / kind)Teaches?
A verifier: tests, a schema or exact-match checkcomputable / oracle_pass or oracle_failYes
A business rule your code checkscomputable / oracle_pass or oracle_failYes
A delayed webhook (a ticket resolved, a payment cleared)resolution / resolved_true or resolved_falseYes
A human reviewcorrection / accepted_unchanged or rejectedYes
Self-rating: the agent or a model grading its own answer, or a thumbs-upproxy / thumbsNever
You do not knowsend nothing—

Self-rating is a weaker signal. It is recorded and can help rank, but it never teaches. An HTTP 200, or a tool call that returned, is not task success. Missing feedback stays unknown.

curl -sS -X POST "$UR_BASE_URL/v1/requests/$REQUEST_ID/outcomes" \
  -H "Authorization: Bearer $UR_API_KEY" -H "Content-Type: application/json" \
  -d '{"channel":"computable","kind":"oracle_pass","source_event_id":"<task id>"}'

The answer (HTTP 202) has a status and a durable receipt:

  • accepted: recorded now. duplicate: a retry of an outcome already recorded, so nothing changes.
  • pending: the gateway has not recorded the request yet, which is normal right after the response. The outcome is stored and applied when the request lands; the receipt tells you when.

source_event_id makes the report idempotent: a retry with the same id never counts twice (the same id with a different body is a 409). A delayed outcome works the same way, days later, on the stored request id. More recording modes (batch, warehouse backfill, the durable queue) are in Record outcomes.

Check the receipt (ocr_…) by its id, or every receipt of a request:

curl -sS "$UR_BASE_URL/v1/outcome-receipts/$RECEIPT_ID" -H "Authorization: Bearer $UR_API_KEY"
curl -sS "$UR_BASE_URL/v1/requests/$REQUEST_ID/outcome-receipts" -H "Authorization: Bearer $UR_API_KEY"

A receipt moves from pending to applied. rejected, failed and expired are final and carry a reason; expired with request_not_found means no request with that id appeared in your workspace within 15 minutes. Its episode says whether the outcome reached the episode it belongs to.

Turn on learning

Learning is off until you turn it on for a call type, and the learning routes take the call type's id (ct_…), not its slug. Look the id up once the first request has created the call type:

curl -sS "$UR_BASE_URL/v1/call-types" -H "Authorization: Bearer $UR_API_KEY" \
  | jq -r '.data[] | select(.slug == "my-agent") | .id'
export UR_CALL_TYPE_ID="<the ct_ id printed above>"

Then turn it on, with a key that has config:write (or as an owner or admin):

curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/enable" \
  -H "Authorization: Bearer $UR_API_KEY" -H "Content-Type: application/json" \
  -d '{"reflection_model":"<a model the workspace owns>","promotion":{"mode":"evaluate"}}'

The route checks what Before you start set up and names what is missing:

AnswerCauseFix
409 corrections_consent_requiredCorrections or knowledge consent is off.The owner chooses Allow learning in the dashboard or uses the session-authenticated PUT /v1/workspaces/{id}/self-learning with {"enabled":true}.
400 reflection_model_not_workspace_ownedThe model is not served by a key your workspace stored or by one of its custom providers (or prepaid billing funds with credits).Add a key under Providers & BYOK, or pick a model one of your keys serves.
403The key lacks config:write, or you are not an owner or admin.Use an owner or admin session, or a key with config:write.
404No call type with that id in your workspace.Use the ct_… id from GET /v1/call-types.

reflection_model is required. promotion, lessons_mode, lessons_k and recipe are optional. With the promotion mode evaluate, the default, a learned candidate is never activated just because it was produced. Each episode is assigned the candidate or the current version, and the candidate becomes active only if it wins under a rule fixed before the evaluation starts. A negative or inconclusive result keeps the current version.

Watch, pause or roll back

Read the status. state is off until learning is on, then collecting (waiting for evidence), learning (a reflection is due or an evaluation runs), paused, disabled, or unavailable, quota_exhausted or budget_exhausted with a reason. reflection.provider says whether the reflection model is usable and, if not, why. evidence separates episodes that arrived, were verified, and were used by the active version, plus the backlog; outcome_receipts counts receipts by state.

curl -sS "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning" -H "Authorization: Bearer $UR_API_KEY"
curl -sS "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/evaluations" -H "Authorization: Bearer $UR_API_KEY"
curl -sS "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/versions" -H "Authorization: Bearer $UR_API_KEY"

Each evaluation shows the rule it was judged by, the two arms, the interval and the decision. An inconclusive evaluation is the system working, not a failure.

Every control is audited and needs config:write (or an owner or admin):

# Pause: stop reflecting and evaluating, keep serving the active version (resume undoes it)
curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/pause" -H "Authorization: Bearer $UR_API_KEY"
curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/resume" -H "Authorization: Bearer $UR_API_KEY"

# Disable: stop serving learned lessons (a running evaluation stops too)
curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/disable" -H "Authorization: Bearer $UR_API_KEY"

# Roll back: point the call type at an earlier version (omit version_id for the previous active one)
curl -sS -X POST "$UR_BASE_URL/v1/call-types/$UR_CALL_TYPE_ID/learning/rollback" \
  -H "Authorization: Bearer $UR_API_KEY" -H "Content-Type: application/json" \
  -d '{"version_id":"<an earlier version id>"}'

Remove it entirely

  1. Disable learning for the call type (above).
  2. Point your client back at its previous base URL and remove the outcome report.
  3. If you added the MCP servers: ultrarouter-mcp uninstall --host claude --with-docs or ultrarouter-mcp uninstall --host codex --with-docs (the plugins have their own uninstall, on MCP servers for Claude Code and Codex).
  4. Revoke the API key in the dashboard Settings. Revoking corrections consent deletes the stored episodes.

What learning does not do

  • It never changes model weights. It adapts the context ultrarouter serves your agent.
  • It never learns from self-rating, a click or the gateway's own signals; only the outcomes you report from a trusted source teach.
  • It never shares what it learned with another workspace.
  • It never promises an improvement. See the evidence status for what is and is not demonstrated.