Odile Gate

Stop your coding agent from burning $500 in an API loop

Gate can refuse repeated identical API calls before they reach your upstream or use your Gate call allowance.

1. What a loop costs

Suppose a model call costs $0.05 and a loop sends 200 calls a minute for 60 minutes: 200 × 60 = 12,000 calls, and 12,000 × $0.05 = $600. That price is an arithmetic example, not a provider quote.

2. How Gate detects a loop

For each app, Gate compares the HTTP method, the SHA-256 of the request path after the project prefix, the query string and the SHA-256 of the request body. Request headers are not part of this loop signature; GET and HEAD have no body hash, while other methods hash the body bytes, including an empty body.

The comparison is exact: changing query order or JSON whitespace changes the signature. It tracks consecutive identical calls that reach the detector, not all calls to the same route template.

By default, the tenth identical call within 10 seconds of the first trips the loop rule; the first nine can pass if the other checks allow them. There is also an earlier cadence check: from the fifth identical call, the last four gaps trip it when their mean is at least 1 ms but less than 250 ms and their standard deviation is less than 15 ms.

A stopped call receives HTTP 429, JSON with error, retry_after_seconds: 30 and recovery, plus x-gate-refused: loop_detected, x-odile-gate-loop-detected: true and Retry-After: 30. The recovery instruction is wait_then_retry_once; its delay is the larger of 30 seconds and the effective loop window.

Every further identical call that reaches the detector extends the block from that call's time. A pause longer than the effective window clears it, or a different signature reaching the detector starts a new sequence; that different call still has to pass the other checks.

Keep observe_only: false to enforce the stop: observe-only mode records loop_observed and continues to the remaining checks. The anomaly guard defaults to alert mode; if configured in strict mode, it temporarily uses at most 3 repeats and a window of at least 10 seconds until 30 minutes after the latest detected anomaly, then restores the saved loop settings.

Loop state is in memory per project object and resets if that object is evicted. Only requests sent through Gate are checked; this detector is not a dollar spending cap.

3. What the agent sees

This transcript comes from the in-memory test app and its fake upstream, which returns {"ok":true}. It uses the default loop settings, sends identical calls 500 ms apart and records the actual request path, request JSON, response status and response JSON; authentication headers are omitted.

POST /g/p000000000000000000000000/chat/completions
{"model":"example","input":"hello"}
HTTP 200
{"ok":true}

POST /g/p000000000000000000000000/chat/completions
{"model":"example","input":"hello"}
HTTP 200
{"ok":true}

POST /g/p000000000000000000000000/chat/completions
{"model":"example","input":"hello"}
HTTP 200
{"ok":true}

POST /g/p000000000000000000000000/chat/completions
{"model":"example","input":"hello"}
HTTP 200
{"ok":true}

POST /g/p000000000000000000000000/chat/completions
{"model":"example","input":"hello"}
HTTP 200
{"ok":true}

POST /g/p000000000000000000000000/chat/completions
{"model":"example","input":"hello"}
HTTP 200
{"ok":true}

POST /g/p000000000000000000000000/chat/completions
{"model":"example","input":"hello"}
HTTP 200
{"ok":true}

POST /g/p000000000000000000000000/chat/completions
{"model":"example","input":"hello"}
HTTP 200
{"ok":true}

POST /g/p000000000000000000000000/chat/completions
{"model":"example","input":"hello"}
HTTP 200
{"ok":true}

POST /g/p000000000000000000000000/chat/completions
{"model":"example","input":"hello"}
HTTP 429
{"error":"Loop intercepted: coding-agent sent the same POST call 10 times. Odile Gate stopped it before it reached your upstream.","retry_after_seconds":30,"recovery":{"retry_after_seconds":30,"do":"wait_then_retry_once","hint":"Pause the loop, then retry this call once after the delay."}}

The tenth call does not reach the fake upstream or increment Gate's call usage. For preventing a repeated write after a lost response, read the safe retries guide.

4. What the owner sees

With local audit storage enabled, this stop adds an entry with app_name: coding-agent, method: POST, route: POST /chat/completions, status: 429 and reason: loop_detected, alongside its time, duration, body size and path/body hashes. The project summary API reports loops.stopped_last_30d from stored audit rows; see the audit guide for inspection, exports and storage limits.

Configure alerts.webhook_url and alerts.threshold under gateway in declarative config, or use PUT /api/projects/<id>/alerts with your admin key and webhook_url and threshold fields. The dashboard's Alerts form configures the same webhook and threshold.

When a sequence first becomes blocked, Gate queues a JSON POST with type: gate.loop_detected, project and app identity, time, route, count, detection method and interval statistics. Loop notifications use a separate limit of one per app per ten-minute clock bucket and do not wait for the refused-call threshold; without a webhook URL, no webhook is sent.

The threshold defaults to 20 and accepts 5–1,000. It controls gate.blocked_spike: an app's responses with status 401, 403 or 429 in a rolling ten-minute window, including those statuses from upstream; that alert waits at least an hour after the app's last recorded alert.

5. Setting it up in one minute

Run the built-in Demo Desk quickstart to create a Free project and try an allowed read, a refused delete, a repeated-call loop and the audit log:

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

Or connect your coding agent to Gate's MCP server:

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

For your own API, create a project with its upstream base URL, register an app with allowed routes, then send its requests to https://gate.odilelabs.com/g/<project id>/<API path> with Authorization: Bearer <app key>.

The loop rule is configurable per app inside the project through this admin API; defaults are 10 repeats and 10 seconds. It accepts integer repeats from 3 to 100 and integer windows from 1 to 60 seconds, and saving clears that app's current loop state.

PUT /api/projects/<id>/apps/<app>/loop
{"repeats":10,"window_seconds":10}

Authenticate that settings request with the project admin key; keep it on your backend. The app's key is for gateway calls.

6. Plans

Plans and setup · Open your dashboard