Odile Gate

A key, write limits and an audit log for every AI-built app

Put Gate in front of your database API. Every AI-built app gets its own key, a list of the calls it may make, a rate limit, and every call lands in an audit log. Ten minutes, no platform team.

Free, no sign-up: scan an agent config for exposed API keys. It runs in your browser and sends nothing.

How it works

  1. Create a project. Give Gate your API's base URL and, if it needs one, the secret header it expects. You get an admin key, shown once.
  2. Register each app. Name it, list the calls it may make (GET /orders/:id, POST /orders, GET /reports/*) and set its calls per minute. You get that app's key, shown once.
  3. Point the app at Gate. The app sends its own key. Gate checks the key, the call and the limits, adds your upstream secret, forwards the call and writes the audit row.

A call through Gate

Replace <project-id> with your project ID and <app key> with that app's key before running this command.

curl "https://gate.odilelabs.com/g/<project-id>/orders/42" \
  -H "Authorization: Bearer <app key>"

Gate answers refused calls itself: 401 for a missing or revoked key, 403 when no rule allows the call, 429 with Retry-After at the per-minute limit, 429 when the month's calls are used up, and 502 when your API does not answer within 30 seconds. The app's key never reaches your API.

What Gate checks, and what your API still checks

Gate checks the app's key, whether the method and path match one of your rules, and the limits. A plain rule such as GET /orders/:id lets that app call that endpoint for any order; it does not know who the person using the app is.

Per-user rules (Free and Pro)

To keep each signed-in person to their own rows, the app sends that person's login token (from Supabase, Clerk, Auth0, Firebase or any provider that publishes its keys) in x-odile-user-token. Gate checks the signature against your provider's public keys, the expiry and the issuer, and then only lets the call through when it names that person:

curl -X PUT https://gate.odilelabs.com/api/projects/<project-id>/enterprise   -H "Authorization: Bearer <admin key>" -H "content-type: application/json"   -d '{"user_tokens":{"jwks_url":"https://<your-project>.supabase.co/auth/v1/.well-known/jwks.json","issuer":"https://<your-project>.supabase.co/auth/v1"}}'

routes: ["GET /users/{user}/orders/:id", "GET /orders?user_id=eq.{user}"]

With a valid token for user alice, /users/alice/orders/7 passes and /users/bob/orders/7 gets 403; /orders?user_id=eq.alice passes, and leaving out the filter or naming anyone else gets 403. A missing, expired or forged token gets 401. The token is never stored and never sent on to your API. Gate accepts RS256, ES256 and EdDSA tokens; audience and claim (default sub) are optional.

Query pins scope reads, updates and deletes. They are refused on POST rules, because an insert takes its owner from the body; for writes put {user} in the path (POST /users/{user}/orders) and have your API take the owner from the path.

Try it without a login provider. On a Demo Desk project (upstream https://sandbox.gate.odilelabs.com), set user_tokens to the demo login and give an app the rule GET /customers/{user}/tickets/:id. Then curl "https://gate.odilelabs.com/demo-login/token?user=A" returns a 10-minute token for customer A: with it /customers/A/tickets/1 passes and /customers/B/tickets/4 gets 403.

-d '{"user_tokens":{"jwks_url":"https://gate.odilelabs.com/demo-login/jwks.json","issuer":"https://gate.odilelabs.com/demo-login","audience":"gate-demo"}}'

Anyone can get a demo token for any user, so Gate accepts the demo login only on Demo Desk projects; a project with your own API needs your own provider.

What it still cannot do: if a call does not name the user (for example GET /orders/42 on its own), Gate cannot know who owns order 42. Put the user in the path or the query, or keep that check in your API.

Response redaction, Free and Pro: choose the JSON fields each app may receive in the dashboard or with gate_set_response. Unselected fields are removed. JSON only, up to 1 MiB and 5 seconds to read; unsafe replies are withheld. Upstream writes still count. Response filtering guide.

Check the setup before giving an agent production access

Gate Setup Check reads the real protection settings and gives your agent and its human the same report: what is configured, what needs attention and how to fix it. It catches observation mode, writes without mandatory runs, missing JWT setup, wildcard grants and approval separation gaps. It sends no upstream requests and changes no settings.

Use gate_check_setup from your coding agent, open the dashboard, or read the setup-check guide. Free and Pro. Configuration analysis with explicit limits, followed by your real integration tests.

Operational metrics, Free and Pro: the dashboard and read-only gate_metrics tool show the last 24 hours of stored calls by HTTP status and reason, measured duration including upstream wait, and separate monthly gateway usage. Audit coverage can be partial. Read the metrics and audit guide.

AI model API keys, Free and Pro: put Gate in front of your model provider so each agent gets its own key, only the calls you allow, a per-minute limit and the loop stop, while your provider key stays in Gate and can be replaced in one call. Prompts and answers are never stored. Read the model API key guide.

Gate Actions: give your agent permission to do the job, not access to everything

For an agent that changes real customer data. Your backend grants each agent session a run: the few actions it may take, bound to one customer, with limits and an expiry. Gate enforces it on every call, on top of the app's own allow-list.

Add a person under Approvers in the dashboard before granting approval:true actions. Without an active approver, Gate refuses the run with 409.

curl -X POST https://gate.odilelabs.com/api/projects/<project-id>/runs   -H "Authorization: Bearer <admin key>" -H "content-type: application/json"   -d '{"app":"app_...","label":"triage ticket 4411","scope":{"customer":"A"},"ttl_seconds":1800,
       "actions":[{"rule":"PATCH /customers/{customer}/tickets/:id","max":10},
                  "GET /customers/{customer}/tickets/:id",
                  {"rule":"POST /customers/{customer}/messages","approval":true}]}'

The answer has a run token (gr_..., shown once). The agent sends it as x-odile-run with its app key. GET /g/<project-id>/_gate/run tells the agent what it may still do.

Refused

Customer B's tickets (action_not_in_run). Deleting a ticket, or exporting, even if the app's allow-list has them. The 11th update (run_limit_reached). Anything after the run expires or is revoked.

Approved, exactly

A call marked approval is held, not sent (202 approval_required). A person sees the exact call in the dashboard and approves or rejects it with an approver key. The agent resends it with x-odile-action; a changed method, path, query, forwarded header or body is refused (approval_mismatch), and it never runs twice (already_executed).

Retries that never run twice

Every write in a run carries an Idempotency-Key. A retry after success gets already_executed with the first status. After a timeout or a 5xx Gate answers outcome_unknown instead of sending the write again, and POST /_gate/recover asks your API whether it happened: executed, safe to retry, or needs review.

Make runs mandatory for an app's writes with PUT /api/projects/<id>/apps/<app>/runs {"mode":"writes"} (or "all"). Included on Free and Pro.

Approvers. Add the people who approve (POST /api/projects/<id>/approvers {"name":"Dana"}, or the dashboard) before granting approval actions. Each gets their own key and signs in to a dashboard that shows only what waits for them. Only an active approver key can approve, reject or reopen actions; the admin key cannot decide, even after the last approver is revoked. Every decision is in the change log with who made it.

Try it in one minute, no card

curl -s https://gate.odilelabs.com/actions-quickstart.sh | sh

It runs against a built-in demo ticket API (nothing leaves Gate): an allowed update, another customer's ticket refused, a retry that does not apply twice, an exact approval, revocation and expiry. Then use the client in your agent: import { Gate } from 'https://gate.odilelabs.com/sdk/gate.mjs' (no dependencies; Node 18+, Deno, Workers, browsers).

Guides: stop an agent from changing the wrong customer's data · approve one exact agent action · agent retries without duplicate writes · give each agent its own API key · read only the signed-in user's data · see every call your agent made.

What it does not replace: your API's own ownership checks (or Supabase row-level security). Gate checks the method, path and query, not what is inside a body. It only protects calls that go through Gate, so give the agent no other credentials. And "never twice" means Gate never sends it twice; your API should honour the forwarded Idempotency-Key too. Keep the admin key with your backend, approver keys with the people who approve, and neither with the agent. Responses carry Server-Timing: gate;dur=…, upstream;dur=… so you can measure what Gate adds.

Hand off one smaller job

An agent can delegate a narrower run to another active app in the same project: POST /g/<id>/_gate/run/delegate with the parent's app key and x-odile-run. Send {app, actions, ttl_seconds, max_calls, label}. Scope is inherited, approvals stay required, the child app's allow-list still applies, and expiry never exceeds the parent's. Every executed call spends the child's and all ancestors' budgets; revoking or expiring any ancestor stops the descendants. Delegation stops at depth five.

Discovery includes parent_run_id and depth. Admins can read GET /api/projects/<id>/runs/<run>/tree. The SDK offers gate.delegate(spec); pass the returned token with the destination app's key.

A refusal with a next step

Loop, exhausted-run and unknown-write refusals include recovery: {retry_after_seconds, do, hint}. A loop says wait_then_retry_once; a used-up run says stop; an unknown write says call_recover when a check exists, otherwise ask_human. Status codes and reason headers stay the same. The SDK exposes result.recovery and never retries automatically.

Learn the smallest rules from traffic

An admin opts in for one app with PUT /api/projects/<id>/apps/<app>/learn {"minutes":60}: default one hour, maximum 1440 minutes (24 hours); use zero to stop. During that window only, learning records method + normalized path templates (id-like segments become :id), template totals and call counts per minute. No raw ids, query values, headers or bodies are recorded by learning. At most 500 templates are kept, then deleted 7 days after learning ends. Calls that Gate refuses can contribute suggestions too; review them before granting permission.

GET .../learn returns suggested routes and per_minute: p99 of observed minute counts times two, at least 10 and at most 6000. An admin must confirm POST .../learn/apply {"confirm":true,"mode":"extend"} (or "replace") to apply the routes and rate. The SDK's backend-only methods are learn, learnSuggestions and applyLearning. MCP's gate_learn_suggestions only reads.

Keep an audit you can verify

New stored audit rows form a SHA-256 chain from a recorded genesis; older rows remain outside that chain. GET /api/projects/<id>/audit/proof?from=1&to=100 exports row-id ranges, chain anchors and an HMAC-SHA256 signature derived from the project key material. Up to 10,000 rows per page; follow next_from. POST /api/projects/<id>/audit/verify with the signed export reports intact or the first broken row (one-based position). Approval decisions include the approver name and decision time. Save exports outside Gate to keep evidence beyond your plan's retention. Projects with store_audit=false have no chain on Gate. The SDK provides auditProof and verifyAudit on GateAdmin.

Advisory approval risk

When a project has an approval-risk model, approvers see an estimated rejection risk and the top two contributing signals. The pending queue sorts by risk and refreshes its features. Only a person approves or rejects; the score never executes a call. Without a model, scores are absent and newest-first order stays unchanged.

The admin can install a small JSON tree model with PUT /api/projects/<id>/risk-model, or remove it with JSON null. The offline tools/ml/train_risk.py --export saved-audit.jsonl trainer requires at least 200 approved/rejected actions, 30 rejects and test AUC at least 0.05 above the app-history baseline. It reports chronological test AUC, precision/recall for the top 20%, Brier calibration and feature importance. Dry run is the default; synthetic tests are not evidence of production accuracy.

Signed proofs include safe action labels with decision-time features. Follow next_actions_after as actions_after separately from the audit next_from cursor, using the API, SDK auditProof({from,to,actions_after}) or MCP gate_audit_proof. Labels exclude held paths/bodies, keys, IPs and notes; old actions without captured features are excluded. Initial decisions survive execution and reopen. Offline HMAC authentication requires a separate check through audit/verify.

See how a run unfolded

Click a run in the dashboard for its live timeline: call time, method, matched rule, status and reason; held, approved and rejected actions with who decided; recovery outcomes; and delegated children. It refreshes every 20 seconds and shows the latest 500 events. The admin API GET /api/projects/<id>/runs/<run>/timeline supports older events with before. It contains no bodies or query values. The SDK offers admin.timeline(runId) and admin.runTree(runId).

Set it up from your coding agent

The agent that writes your app can put Gate in front of your API in the same session: it creates the project, registers each app with only the calls that app makes, and reads the audit log. Nothing to install.

claude mcp add --transport http odile-gate https://gate.odilelabs.com/mcp

Discover the endpoint through the AI catalog and experimental MCP server card. POST initialize and tools/list to /mcp; a plain GET returns 405 because no SSE stream is provided. No OAuth login is needed; administering an existing project requires its admin key. Any MCP client works with the URL https://gate.odilelabs.com/mcp. Examples of tools (use MCP tools/list for the complete current list): gate_create_project, gate_add_app, gate_list_apps, gate_check_setup, gate_audit, gate_explain_refusal, gate_delegate_run, gate_learn_suggestions, gate_audit_proof, gate_audit_verify, gate_run_tree, gate_run_timeline. Keys are shown once; the agent keeps them in your .env. Writing your own client in Python? Send any User-Agent header: Cloudflare refuses Python urllib's default one with error 1010 before the request reaches Gate.

Try it in one minute

This uses the built-in Demo Desk API, registers a reporting app that may only read tickets, then shows an allowed read, a refused delete, a stopped loop and the audit log. It creates one free project.

curl -fsSL https://gate.odilelabs.com/quickstart.sh | sh
Allowed read:     GET /customers/A/tickets/1 -> 200
Forbidden delete: DELETE /customers/A/tickets/1 -> 403 route_not_allowed
Repeated read:    GET /customers/A/tickets/2 -> 429 loop_detected

Read it first if you like: /quickstart.sh.

Why was my call refused?

Every refusal names its reason in the x-gate-refused header and in the audit log.

ReasonStatusWhat to fix
bad_key401Send Authorization: Bearer <app key>; the key may be revoked or rotated.
route_not_allowed403No rule matches the method and path. Add one, e.g. GET /orders/:id; the response lists the method and path Gate saw.
rate_limited429The app's per-minute limit; wait for Retry-After or raise per_minute.
monthly_quota429The plan's calls for the month are used; upgrade or wait for the 1st (UTC).
loop_detected429The same call repeated; fix the retry loop, or loosen it per app with PUT .../apps/<app>/loop.
bad_user_token / outside_user_scope401 / 403Per-user rules: send a valid x-odile-user-token, and name that user in the path or query.
blocked_by_rule / ip_not_allowed / app_suspended403A project block rule, the IP allowlist, or an admin suspended the app.
unsafe_path / body_too_large400 / 413An encoded slash or dot segment in the path, or a body over 10 MB.
bad_run / run_expired / run_revoked401 / 403Gate Actions: send a current run token in x-odile-run, with the key of the app it was granted to.
run_required / action_not_in_run403The app needs a run for this call, or the run does not allow this method, path or customer.
run_limit_reached429The run used its calls, in all or for this action. Grant a new run.
idempotency_key_required400A write inside a run needs an Idempotency-Key; reuse it when you retry that write.
already_executed / outcome_unknown409That write already ran (its status is in the answer), or its outcome is unknown after a timeout: check your API, or ask POST /_gate/recover with the same key when the run action has a check.
no_check / no_such_operation / still_running409 / 404 / 409Recover cannot settle it: the action has no check, the key is unknown in this run, or the write is still waiting on the upstream.
approval_pending / approval_rejected / approval_mismatch409 / 403 / 409Wait for the approver, read their note, or resend the call exactly as approved.
upstream_timeout / upstream_error502Your API did not answer in 30 seconds or could not be reached.

Loop breaker

When an AI agent gets stuck and sends the same call over and over, Gate stops it before it reaches your API or uses your quota. On Free and Pro. You can make it stricter or looser for each app.

A stopped call gets 429 with x-odile-gate-loop-detected: true; any different call still passes. With alerts on, your webhook gets a gate.loop_detected message at once (Slack, Discord or Teams), and the dashboard lists loops stopped in the last 30 days. Gate also tells you when a project reaches 80% and 100% of its monthly calls.

Anomaly guard

Gate scores each app’s calls per minute, refused-call ratio, upstream 429 ratio and identical-repeat ratio against its recent traffic. Scoring starts after 30 minutes of history. The dashboard and gate_metrics show the last score, its driver and anomaly counts in the last 24 hours.

Choose off, alert (default) or strict per app. An anomaly adds an audit event; a configured alert webhook also receives a gate.anomaly message, signed when webhook signing is configured, sharing the refused-call spike alert’s one-per-app-per-hour limit. Loop and approval notifications keep their existing limits. Without a webhook, alert mode records the audit event. Strict mode also tightens that app’s loop breaker for 30 minutes, then restores its configured setting. The score itself never refuses a call.

In the dashboard or GET/PUT /api/projects/<id>/apps/<app>/anomaly, set mode, z_threshold (2–10, default 4), min_minutes (1–30 consecutive minutes, default 3) and half_life (5–1440 minutes, default 60). Unknown keys are errors; strict_until is read-only. Declarative configuration uses plugin anomaly-guard with those same four settings.

For security teams

Annual contracts and a security questionnaire: hello@odilelabs.com.

Using OpenShell?

Bind a signed sandbox identity to a scoped Gate run with our native OpenShell v0.1.5 HTTP-request middleware. Run the TLS adapter beside OpenShell; Gate remains the sole executor for approvals and retry protection. Available with Free or Pro.

One-command OpenShell setup: curl -fsSL https://gate.odilelabs.com/openshell-setup.py | python3 -. Agents use gate_openshell_setup to discover the installer hash and recipe schema. The default is a Free read-only demo; custom scope needs owner authorization. Requires Python and local Docker.

Plans

Free

1 project per admin key
3 apps
10,000 calls a month
Audit log kept 30 days

Pro · $29 a month

25 apps
1,000,000 calls a month, then $10 per extra million (keeps working up to a limit you set)
Audit log kept 365 days

Setup · $99 once

We put one of your existing APIs behind Gate: the project, each app's key and allowed calls, and limits. We show allowed and refused calls working and send you a short guide. Works with Free or Pro.
Buy the setup

What we store, and what we never store

We store

Your route rules. The SHA-256 hashes of your keys, never the keys. Call metadata: time, app, method, the route rule the call matched (for example POST /orders/:id), a SHA-256 of the path, status, duration, body size and body SHA-256, and country. When no rule matches, only the path's first segment and its SHA-256. Your upstream secret, encrypted with AES-GCM. A Gate Actions call that waits for approval: its path and body, encrypted with AES-GCM, only until it runs, is rejected or expires (24 hours at most); then only its hashes and outcome stay.

We never store

Request or response bodies (except, encrypted, a call waiting for your approval). Full request paths. Query string values. Request or response headers. Your users' data in any of those.

Calls without a valid key are counted every hour; the audit log keeps a row for the first 20 of each hour.

Create a project

No API of your own yet? Use the built-in Demo Desk as the upstream: https://sandbox.gate.odilelabs.com (a small ticket API that runs inside Gate; no key needed, nothing leaves Gate). Try GET /customers/A/tickets/1; it also has messages and bookings for customers A and B.

Or with curl:

curl -X POST https://gate.odilelabs.com/api/projects -H "content-type: application/json" \
  -d '{"name":"Shop API","upstream":"https://api.example.com/v1","header_name":"Authorization","header_value":"Bearer ..."}'