The control plane for agent traffic.
One key, one policy, one bill across LLM providers, MCP servers, and REST APIs. Every model and tool call is authenticated, scoped, logged, and accountable — and your vendor credentials never reach the agent.
/v1/*OpenAI-compatible/mcpStreamable HTTPdocker compose upself-host
ob_live_…- Stripe
- GitHub
- OpenAI
- Anthropic
- any MCP
- authenticate
- authorize · Cedar
- receipt
re_01J9X4…ALLOWpol_3ka… rev 14upstream re_3PqK…sampleYour agents already act in production. Nothing is checking the receipt.
Provider keys in env files, OpenRouter for failover, MCP blocks pasted into IDE configs with live tokens, a handwritten Stripe wrapper — and no answer to “which agent called create_refund at 18:03, and under what authority?”
{
"mcpServers": {
"stripe": {
"command": "npx",
"args": ["-y", "@stripe/mcp"],
"env": { "STRIPE_SECRET_KEY": "sk_live_••••" }
},
"github": {
"command": "npx",
"args": ["-y", "@github/mcp-server"],
"env": { "GITHUB_TOKEN": "ghp_••••" }
},
"openai": {
"command": "npx",
"args": ["-y", "openai-mcp"],
"env": { "OPENAI_API_KEY": "sk-••••" }
}
}
}- Secret sprawl
- Agents hold raw sk_live_ keys and OAuth tokens in IDE configs and prompt context. One leak is the full vendor blast radius.
- No argument-level authorization
- API gateways ask “may this token hit this route?” Agents need “may this agent refund this amount in this environment — and has a human approved it?”
- No verification
- A model can claim “I refunded the customer” while the vendor returned requires_action. Nothing catches the lie.
- No unified metering
- Token spend and tool-call spend live in different dashboards. Budgets cannot fail closed across both.
Model routing is solved. The tool path is where the cost and the risk moved.
One key in. Vaulted credentials out.
Point your app at one OpenAI-compatible URL and your IDE at one MCP URL. Obol authenticates the key, filters what the agent can see, authorizes what it can do, injects the vendor credential on the way out, and writes a receipt.
curl https://gateway.tryobol.dev/v1/chat/completions \
-H "Authorization: Bearer ob_live_…" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o",
"messages": [{"role": "user", "content": "…"}]
}'- AuthenticateThe virtual key is hashed and looked up; revoke propagates in 60 seconds or less.
Redis cached KeyContext, ≤60s revoke - Budget & rateSpend and request caps are checked before anything leaves. Caps fail closed.
spend / rpm fail closed - RouteYour own provider key is unwrapped in-process and injected on the outbound hop.
BYOK provider key injected - StreamTokens stream back through the gateway. No database sits on the hot path.
Postgres never on the path - MeterTokens in, tokens out, model, and cost are recorded after the response.
UsageEvent, async
- Virtual key
- An Obol-minted credential (ob_live_…) scoped to a workspace, models, tools, budget, rate limits, env, and expiry; the only credential an agent ever holds.
- Connection
- A workspace's instance of a connector: its OAuth tokens or restricted key, encrypted, plus scopes and status.
- Snapshot
- The compiled per-workspace bundle of keys, connections, tools, and Cedar policy that the gateway evaluates from; the only way state reaches the data plane.
The wrong tool cannot execute.
Cedar decides every tool call on principal, action, resource, and context — arguments included. The policy set compiles into the workspace snapshot and is evaluated in-process, default-deny. We never guarantee the model picks the right tool. We guarantee the wrong one does not run.
stripe.create_refund { amount_usd: 42 }sample- satisfiedprefilter
cel · visible_to(key_7Hq…)listed in tools/list
- satisfiedprincipal
in Role::"support-agent"key_7Hq… → support-agent
- satisfiedaction
== Action::"stripe.create_refund"stripe.create_refund
- satisfiedresource
in Env::"prod"prod
- satisfiedcontext
args.amount_usd <= 5042
allow · pol_3ka… rev 14 · idt_4fd… mintedOne permit matched — including the argument. The gateway mints the idempotency key before the request leaves it, injects the vaulted Stripe credential on the outbound hop, and writes the decision onto the receipt.
// Support agents refund up to $50 unattended.permit( principal in Role::"support-agent", action == Action::"stripe.create_refund", resource in Env::"prod") when { context.args.amount_usd <= 50 }; // Destructive in prod: only with an approval.permit( principal, action in ActionGroup::"destructive", resource in Env::"prod") when { context.approval.granted };Permit one matched on all four: principal, action, resource, and the argument.
packages/cedar, compiled into the snapshot, and evaluated in-process — never a network hop per decision.- Default-deny
- No permit, no call. Cedar is the authorization of record, and its decision, policy id, and revision are written onto the receipt before anything is dispatched. The policy set is compiled data evaluated in-process — never a network hop or a database read per decision.
- Arguments, not just tool names
- context.args.amount_usd <= 50 is a condition Cedar evaluates, not a convention a connector is trusted to honour. The request hash is bound to the decision, so the call that runs is the call that was authorized.
- Held, not failed
- A destructive tool in prod returns approval_required with an approval id and an inbox link — a distinguishable outcome, not an error. On approve, the same invocation and the same idempotency key resume, exactly once.
- Invisible, not merely forbidden
- The CEL prefilter runs before tools/list, so a tool this key may not call never appears in the model's context at all. A denied call and a nonexistent one return the same bytes, so the catalog cannot be enumerated by probing.
A receipt that says what it can prove.
Every authenticated tool call gets one gateway-minted receipt, keeping authorization, dispatch, and verification as three separate summaries. Evidence trust is assigned by Obol from the route the call actually took — never from what the response claims about itself. Only gateway_observed earns the word proves, and untrusted can never read verified.
One bounded HTTP request the gateway builds from the tool's schema and sends itself, with the vaulted credential injected on the outbound hop.
strongest
gateway_observedthe receipt rests hereObol built the request, injected the vaulted credential, opened the socket, and read the response itself. The only class that earns the word proves.
vendor_signedThe vendor's own lifecycle webhook, terminating at Obol and checked against per-subscription connection material. The vendor's statement about the vendor's system.
connector_attestedA field derived by a reviewed first-party worker. The worker normalises; it never holds a credential, never calls the vendor, and never assigns its own trust.
broker_attestedA broker-signed statement over the execution, validated against a pinned workspace-scoped key. Non-repudiation, not correctness — a broker is a vendor, never an authority.
untrustedA result from a server Obol does not operate. Recorded in full, correlated, billed — and never, at validation time, enough to reach verified.
Obol's own socket terminated at Stripe, and the response bound to the assertions the tool declared. On this route the receipt proves what the call did.
rcp_01J9X4… · receipt_revision 2sample- invocation_id
- inv_01J9X4…
- tool
- stripe.create_refund
- request_hash
- sha256:9f2c…e1
- idempotency_key
- idt_4fd… · propagated to the vendor
authorizationwhat policy allowed
- decision
- allow
- policy_id
- pol_3ka…
- revision_id
- rev 14
dispatchwhat the attempt observed
- state
- accepted
- evidence_trust
- gateway_observed
- vendor_http_status
- 200
- upstream_id
- re_3PqK… (Stripe)
verificationwhat the evidence supports
- state
- verified
- method
- response_binding
- evidence_trust
- gateway_observed
- evidence_digest
- sha256:c41a…
latency_ms 412 · usd_micros 4000 · created_at 2026-08-29T11:04:18Z- Claimed and observed are different fields
- The model's claim and the vendor's observed status are stored separately, and neither is rounded up. A disagreement resolves to contradicted; missing or ambiguous evidence resolves to inconclusive. There is no optimistic success anywhere in the vocabulary.
- Idempotency is always Obol's
- The gateway mints the key before the call leaves it and holds it across approval, transport retries, and safe redispatch. On a federated route the key may not reach the vendor at all — brokers offer no idempotency primitive, so their retries stay off — and the receipt records which guarantee actually applied.
- One log for tokens and actions
- Usage events and receipts share workspace, key, and policy ids. Filter by any of them and the model calls and the tool calls come back together, on one bill and one audit trail.
- The gateway is the only receipt authority
- No worker, no remote MCP server, and no catalog broker mints a receipt, assigns evidence trust, or declares a call verified. A broker is a credential-custody and catalog vendor. Making the least-trusted component its own auditor is the failure mode this design exists to avoid.
What ships in v1.
Scope is a promise. One line runs the length of it — what the agent can reach, what happens at the gate, what the team gets — and then it stops.
What the agent can reach
Surface area, not connector count.
Unified access
gatewayOne key works on
/v1/chat/completions,/v1/responses,/v1/embeddings,/v1/messages, and/mcp(Streamable HTTP).Catalog without connector debt
catalogOpenAPI import to a generic executor; remote MCP federation with namespacing (
stripe.create_refund); a few hand-built workers where a vendor earns one.View catalog2,148 connectors mapped — coverage, not availability.
What happens at the gate
Every call, in-process, default-deny.
Tool-aware authorization
cedarA CEL prefilter decides visibility — a tool the key may not call never appears in
tools/list. Cedar decides permission, with argument conditions likeamount_usd <= 50. Compiled data, evaluated in-process: no network hop per decision, and the default is deny.Credential brokering
vaultOAuth, restricted vendor keys, BYOK. Per-connection DEK, KEK in your KMS; plaintext exists only inside the gateway process, on the outbound hop. Revoke propagates in ≤ 60 s. Never a pooled vendor account.
Approvals
controlDestructive tools in prod return
approval_requiredunless policy explicitly grants. A human approves in the inbox; a worker executes once, under one idempotency key.Verification & receipts
receiptEvery tool call gets a receipt: policy id, upstream id, request hash, idempotency key, vendor status, latency, cost — and the evidence class, so a gateway-observed call is never confused with a federated one.
What the team gets
One key, one policy, one bill.
Metering & budgets
controlUsage events for tokens and tool calls; caps fail closed on both planes; one invoice.
Self-host from day one
infraOne
docker compose up, your KMS or a local key. OSS-core plus a hosted cloud.Not in v1, on purpose.
- No marketplace
- No 100 providers
- No % of your payment volume
- No pooled vendor accounts
Run it in your VPC. Same binaries, your KMS.
Dev starts with the local setup below. Hosted is the same compose with managed Postgres and Redis. Self-host is the same compose with your keys. The shape below never changes between them — only the labels do.
Postgres, Redis, gateway, control, connectors and web come up together. The key-encryption key is a local age key, so nothing needs a cloud account.
$ git clone <your-obol-repository> obol && cd obol$ make -C infra env$ make -C infra age-key # install age first$ age-keygen -y infra/secrets/age.key # public recipient$ vi infra/.env # configure values below$ make -C infra prod-upgateway listening on :8080 (/v1/*, /mcp)control listening on :8000Install Docker Compose, Make, and age. In infra/.env, set POSTGRES_PASSWORD, OBOL_SERVICE_JWT_SECRET, and OBOL_AGE_RECIPIENT to the public value printed above. Configure your Clerk instance keys and issuer for dashboard sign-in. Follow the setup guide for the remaining deployment settings.
Rust gateway · Python control plane · Postgres + Redis · your KMS (AWS/GCP) or age + SOPS
Bring your own app. BYOA uses a customer-owned OAuth app, so vendor tokens live in your cloud project rather than ours — the enterprise custody option, and never a pooled vendor account.
No seats. Metered calls. Never a cut of your volume.
The subscription is per workspace, or one for the whole startup on Team — never per seat. Usage is priced by what a call can do, and a call the gate refuses costs nothing.
What we bill for
A subscription, never a seat — per workspace, or one for the whole startup on Team. An agent is not a seat, and neither is the engineer watching it.
Dispatched tool calls — metered per 1,000 and priced by what the call can do. A read costs less than a refund.
LLM requests through the gateway — for governing the request — never a spread on your tokens.
What we never bill for
Your model tokens — metered for your budgets, billed by your provider.
Your payment volume — we are not a merchant of record, and never take a percentage.
Calls that never left the gate — denials, retries, fallbacks, approval resumes, and verification probes are free.
Self-host
Available now$0
the same binaries, your infrastructure
One compose file, your VPC, your keys.
- Unlimited workspaces
- Full policy engine and receipts
- Your KMS, or age + SOPS
- No usage metering, no phone home
Pro
Coming soon$49
per month + usage
One workspace, writing to production.
- Hosted gateway and control plane
ob_live_keys with budgets- Cedar policies with argument conditions
- Approvals inbox
- 30 days of receipts
Team
Design partners now$1,500
per month + usage
Dev, stage, and prod for the whole startup.
- Every workspace your startup runs
- SSO and workspace roles
- One year of receipts
- Grandfathered usage schedule
- Assisted onboarding
Enterprise
Talk to usFrom $30,000
per year, committed volume
Your VPC, your KMS, your auditors.
- Self-host with support
- Bring your own app (BYOA)
- SCIM and audit streaming
- MSA, DPA, and an SLA
Prices exclude your own vendor and provider bills, which you keep paying directly. Self-serve checkout is not open yet: Pro opens at public launch, and every other tier starts with the same conversation.
Per 1,000 dispatched calls
A read-only search costs us a policy evaluation and a receipt row. A destructive write costs an approval round-trip, a verification plan, webhook correlation, and the evidence retention that makes any of it worth having. The price says so.
- Read-only toolsretrieve, list, search
- $0.30
- Idempotent writessafe to repeat under one key
- $1.00
- Destructive toolsapproval, verification, receipt revision
- $3.00
- LLM requestsgovernance, not inference
- $0.10
These are the public rates. Design partners on Team are billed on a separate grandfathered schedule, not a volume discount from this list.
One logical call is metered once, whatever it took to make it. Internal retries, provider fallbacks, approval resumes, and receipt revisions never add a charge, and neither does a call policy refused before dispatch — so nothing here pays us to route more, retry more, or pick the costlier provider. A vendor error after dispatch is a governed call and is billed.
Questions engineers ask first.
Short answers, in the product's own terms. The long version is in the architecture and the ADRs.
- No. Inbound credential is never the outbound credential. The agent holds an Obol virtual key; your vendor credentials are envelope-encrypted (a DEK per connection, the KEK in your KMS) and decrypted only inside the gateway process on the outbound hop. They never appear in logs, worker configs, or the dashboard.
Pay the toll once. Pass every gate.
We're onboarding design partners in small batches. Tell us what your agents touch — models, MCP servers, the APIs they write to — and we'll take it from there.
Prefer to read first? How it works · Self-host · FAQ