Technical Guide
Govern an OpenRouter Agent with OpenBox: Integration Guide
Wrap an OpenRouter agent with OpenBox in a few steps: evaluate every call, enforce a provider allowlist, and prove which provider served each prompt.
Published on


How to Govern an OpenRouter Agent with OpenBox
Wrap your existing OpenRouter agent with two calls, then show which provider served every prompt, with evidence a reader can re-check at the gateway. A step-by-step integration.
By Tahir Mahmood, Co-founder & CTO, OpenBox · Last updated 8 September 2026
You already route through OpenRouter, a model-routing gateway, for reliability: if one upstream provider is down, your call is served by another. The trade is that the provider serving any single call is chosen at request time and is not returned in the model response by default. OpenRouter can surface it: an optional Router Metadata field on the response reports the routing decision inline, and a separate Generation API reports the provider, region, and cost for each call, keyed to a generation ID. But reading it back for every call, checking it against what you promised, and sealing it as evidence is work most teams never wire up. So a promise that a customer’s data stays with one provider is hard to actually show was kept.
This guide adds a governance layer to that agent. In a few steps you will evaluate every model call and tool before it runs, enforce a provider allowlist the gateway fails closed on, and produce a sealed, re-checkable record of which provider actually served each prompt. Your agent code barely changes: you wrap two calls.
What you will need
Three things before you start:
• Node.js 18.17 or later.
• An OpenRouter API key (it looks like sk-or-…). One key is enough, as explained in Step 4.
• An OpenBox account and agent credentials, created in the dashboard: an API key, an agent DID, and the agent private key.
How the integration fits together
OpenBox, an AI agent governance platform, wraps the OpenRouter Agent SDK by governing callModel and your tools. Your agent keeps working as it did. OpenRouter already publishes each call’s routing record through its Generation API; what OpenBox adds is collecting that record automatically, comparing it to what you asked for, and sealing it with the session. Concretely, OpenBox adds three things around each call, and they are distinct.
Enforce. A routing policy names the providers you trust, and the SDK writes that list into the outgoing request as OpenRouter’s provider allowlist, so the gateway refuses rather than falling back to a provider you did not approve.
Attest. For every governed call the gateway serves, the SDK reads OpenRouter’s own generation record and seals it into the session’s Merkle tree under a signed root, so the figures cannot be quietly edited after the session closed.
Check. What you requested travels alongside what the gateway did, and every call the gateway serves keeps OpenRouter’s generation ID, so the facts can be re-checked at the gateway rather than taken from OpenBox.
Step 1: Install the SDK
Add the OpenBox governance package and the OpenRouter Agent SDK. @openrouter/agent is a peer dependency.
bash
npm install openbox-openrouter-governance @openrouter/agent |
Step 2: Start from your existing agent
Here is an ordinary OpenRouter agent using the Agent SDK’s callModel. Nothing is governed yet. The model slug is illustrative; use whichever OpenRouter model you already call.
src/agent.ts
import { OpenRouter, callModel, tool } from ‘@openrouter/agent’;
const client = new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY });
const tools = [myTool];
const result = await callModel(client, { model: ‘anthropic/claude-sonnet-5’, input: ‘Summarize the latest incident report’, tools, });
console.log(await result.getText()); |
Step 3: Wrap it with OpenBox governance
Create a governance wrapper, pass your tools through openbox.tools(), and route the model call through openbox.callModel(). Two lines change, plus a close at the end.
src/agent.ts
import { OpenRouter, callModel, tool } from ‘@openrouter/agent’; import { createOpenBoxGovernance } from ‘openbox-openrouter-governance’;
const client = new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY });
const openbox = createOpenBoxGovernance({ agentName: ‘research-agent’ });
const tools = openbox.tools([myTool]);
const result = await openbox.callModel(callModel, client, { model: ‘anthropic/claude-sonnet-5’, input: ‘Summarize the latest incident report’, tools, });
console.log(await result.getText());
await openbox.close(); |
What each part does, per the OpenBox docs: openbox.tools() governs each tool end to end, evaluated before it runs, held open across a human approval, and recorded when it finishes. openbox.callModel() governs the model call and returns OpenRouter’s own result untouched, so getText(), getTextStream() and every other consumption pattern keep working.
Always await openbox.close(). It drains in-flight telemetry and the routing records that arrive just after the last answer, so a run whose last turn finishes instantly may take a few extra seconds to close.
Step 4: Set your environment variables
The wrapper reads five variables. Four are OpenBox settings (the API URL, plus your API key, agent DID, and agent private key); the fifth is your OpenRouter key.
.env
OPENBOX_API_KEY=obx_… OPENBOX_API_URL=https://core.openbox.ai OPENBOX_AGENT_DID=did:aip:… OPENBOX_AGENT_PRIVATE_KEY=… OPENROUTER_API_KEY=sk-or-… |
One detail matters for provenance: OPENROUTER_API_KEY does double duty. Your agent calls the gateway with it, and the SDK reads each call’s generation record back with it. Without a key, routing provenance is inert and the rest of governance is unaffected.
Step 5: Register the agent on the OpenRouter framework
In the dashboard, choose OpenRouter as the framework when registering the agent. This matters more than it looks: the Provenance tab and every routing figure are scoped to agents on this framework, because the routing record only exists for gateway-routed calls. An agent that routes through OpenRouter but is registered as something else will show nothing until the framework is corrected.
Step 6: Run the demo to see every routing outcome
A demo agent ships with the SDK. Its routing showcase makes four real calls against the real gateway, chosen so every routing outcome appears, including the one that only shows up when a call is refused. Total cost is a few tenths of a cent.
bash: setup
cd demo-agent cp .env.example .env # add OPENROUTER_API_KEY and your OpenBox credentials npm install |
bash: run
npm run agent # one prompt, CLI npm run agent:stream # stream the answer npm run ui # web UI on http://localhost:4545 npm run demo:routing # the four-leg routing showcase |
The four legs of demo:routing, each a real API call:
Leg | What it does | Outcome |
|---|---|---|
1. Unconstrained | No allowlist | Provenance is recorded, but nothing was promised: neither a pass nor a failure. |
2. Honored via OpenAI | Allowlist naming the provider that serves the model | honored, served by OpenAI. |
3. Honored via Azure | Same model, a different real upstream provider | honored, served by Azure, so the per-provider latency comparison has something to compare. |
4. Fail closed | An allowlist no provider of this model satisfies | The gateway refuses rather than serving from outside it. No provenance at all, and the session closes carrying its reason. |
Each leg prints what its provenance actually recorded, for example:
output
[provenance] 2 call(s) · served by Azure · region global · $0.00009 · HONORED [provenance] model openai/gpt-4o-mini · ran openai/gpt-4o-mini · AS REQUESTED [provenance] verify at source: gen-1787916416-4xBml5qXbE2Pd0rBYA8q |
Step 7: Read the evidence
Three views hold the result. Read coverage before any rate: a perfect honored rate over traffic that promised nothing means nothing.
Where | What you should see |
|---|---|
Agent → Provenance | All four legs, two providers in the breakdown, a coverage rate below 100% because leg 1 promised nothing, and no dishonored calls. |
Agent → Verify | Pick a session to see that session’s calls and its banner. |
Verify → Run Receipt | Build the receipt and copy a share link to its public page. |
The clean result is easy to misread as a broken one, so it is worth stating: legs 2 and 3 are honored by two different real providers; leg 1 is unconstrained and excluded from the honored rate; leg 4 has no provenance and closes as a failed session carrying its reason, because OpenRouter enforced the allowlist server-side and refused rather than serving from outside it. No dishonored call anywhere is the correct outcome. To see the alarm fire, the failure has to be staged deliberately, because the gateway will not produce one for you.
A run receipt gathers one session’s evidence into a shareable page: which providers served the calls, in which regions, whether the allowlist held, and whether any model was substituted. It carries the generation ID for each call that produced one, so a reader with OpenRouter API access can re-check the facts at the gateway without an OpenBox account. Message content is never in the routing record, which is what makes a public page possible.
Step 8: Enforce a provider allowlist from a policy
In the demo, leg 4 was a hand-written request. In production you want that to come from a routing policy instead. The simplest useful rule refuses any call that names no allowlist, decided before the prompt goes out.
policy.rego: require an allowlist
result := { “decision”: “BLOCK”, “reason”: “routing must be constrained”, } if { routing routing.attributes[“openbox.routing.declared”] == false } |
Rather than only refusing, a policy can attach the routing to use instead. The SDK writes that allowlist into the outgoing request, so OpenRouter itself refuses rather than falling back to a provider you did not approve. A directive can only ever narrow what the caller named, never widen it.
policy.rego: narrow to trusted providers
result := { “decision”: “BLOCK”, “reason”: “this agent may only send prompts to openai”, “patch”: {“new_input”: {“provider”: {“only”: [“openai”], “allow_fallbacks”: false}}}, } if { routing {lower(p) | some p in object.get(routing.attributes, “openbox.routing.requested_only”, [])} != {“openai”} } |
What can be enforced, and what can only be observed
The two routing constraints are not equally strong, and knowing which is which keeps the evidence honest.
Constraint | Strength | Why |
|---|---|---|
Provider allowlist | Enforced, pre-flight | The allowlist travels in the request, so a call no approved provider can serve fails closed rather than falling back to one you did not approve. |
Model requested vs served | Checked, near-total coverage | Nearly every request names a concrete model, so the comparison covers almost all traffic. A call on openrouter/auto is reported as unchecked, never a pass. |
Approved processing region | Observed, one call late | No generic region field in a standard request steers where a call lands. A breach shows on the record afterwards, and a policy can refuse the next call and halt the session. |
On region: this does not mean a gateway cannot pin one. OpenRouter separately offers in-region routing for the EU and US on its Business and Enterprise plans, per-request controls for zero data retention and provider data collection, and region-specific provider endpoints you can target by slug, per its provider routing documentation. What a standard request has no field for is a general region parameter, so OpenBox observes the region the gateway reports and compares it to the regions your policy approved, halting the session one call after a breach.
Where this sits in OpenBox governance
Routing is one input to the wider model. OpenBox wraps an agent in a Trust Lifecycle of five stages, Assess, Authorize, Monitor, Verify, Adapt, and routing evidence lives in Verify. Enforcement uses the four governance decisions OpenBox applies to any operation, in precedence order HALT, then BLOCK, then REQUIRE_APPROVAL, then ALLOW.
The sealing uses the same cryptographic attestation as every session: each event is hashed with SHA-256, combined into a Merkle tree, and the session root is signed, using ECDSA NIST P-256 via AWS KMS by default or an external attestation service you run. Sealing shows the record was not altered after the session closed; it does not prove OpenBox recorded the facts correctly. That is why each served call keeps OpenRouter’s generation ID, which anyone with OpenRouter API access can re-check at the gateway:
bash: verify at source
curl -H “Authorization: Bearer $OPENROUTER_API_KEY” \ “https://openrouter.ai/api/v1/generation?id=gen-1787916416-4xBml5qXbE2Pd0rBYA8q” |
Troubleshooting the integration
A few things that trip people up on the first run:
• The Provenance tab is empty. Confirm the agent is registered on the OpenRouter framework; the tab is scoped to it. Also note provenance is written shortly after the response, so a very recent run may still be collecting.
• A lookup right at completion returns nothing. The generation record lands just after the answer; the SDK retries with backoff and drains before the session closes, which is why await openbox.close() is required.
• An auto-routed call shows as unchecked. That is correct. With openrouter/auto you handed the model choice over, so the model check is reported as unchecked rather than as a pass.
• Routing provenance is inert. Set OPENROUTER_API_KEY. Without it the SDK cannot read the generation record, though the rest of governance still runs.
Frequently asked questions
What do I have to change in my agent code?
Two calls and a close. Wrap your tools with openbox.tools(), route the model call through openbox.callModel(), and call await openbox.close() at the end. Your model, input and every result method such as getText() and getTextStream() keep working exactly as before.
Do I need a separate OpenRouter key for OpenBox?
No. One OPENROUTER_API_KEY does double duty: your agent calls the gateway with it, and the SDK reads each call’s generation record back with it. Without a key, routing provenance is inert while the rest of governance is unaffected.
Why is my Provenance tab empty?
Two common reasons. The agent is not registered on the OpenRouter framework, which scopes the tab, or the run just finished. Provenance is written shortly after the response, so a very recent session may still be collecting when you look.
Can I stop prompts reaching a provider I did not approve?
Yes, before the call. A routing policy attaches a provider allowlist, and the SDK writes it into the outgoing request as OpenRouter’s provider.only, so the gateway refuses rather than falling back to a provider outside the list. A policy directive can only narrow the caller’s list, never widen it.
Sources |
OpenBox (docs.openbox.ai), “Getting Started with OpenRouter,” https://docs.openbox.ai/getting-started/openrouter, accessed 8 September 2026. OpenBox (docs.openbox.ai), “Run the Demo,” https://docs.openbox.ai/getting-started/openrouter/run-the-demo, accessed 8 September 2026. OpenBox (docs.openbox.ai), “Routing Policies,” https://docs.openbox.ai/developer-guide/openrouter/routing-policies, accessed 8 September 2026. OpenBox (docs.openbox.ai), “Routing Integrity,” https://docs.openbox.ai/developer-guide/openrouter/routing-integrity, accessed 8 September 2026. OpenBox (docs.openbox.ai), “Run Receipt,” https://docs.openbox.ai/developer-guide/openrouter/run-receipt, accessed 8 September 2026. OpenBox (docs.openbox.ai), “Proof of Routing,” https://docs.openbox.ai/developer-guide/openrouter/proof-of-routing, accessed 8 September 2026. OpenBox (docs.openbox.ai), “Governance Decisions,” https://docs.openbox.ai/core-concepts/governance-decisions, accessed 8 September 2026. OpenBox (docs.openbox.ai), “Attestation & Cryptographic Proof,” https://docs.openbox.ai/administration/attestation-and-cryptographic-proof, accessed 8 September 2026. OpenRouter, “Agent SDK,” https://openrouter.ai/docs/agent-sdk/overview, accessed 8 September 2026. OpenRouter, “Provider Routing,” https://openrouter.ai/docs/guides/routing/provider-selection, accessed 8 September 2026. OpenRouter, “Get request & usage metadata for a generation,” https://openrouter.ai/docs/api/api-reference/generations/get-generation, accessed 8 September 2026. |

