Your AI Gateway Blocked the Request. Did You Still Pay?
An automation you built runs overnight. Something in the loop goes wrong, it retries, and by morning it has called an expensive model eleven thousand times. That is the scenario every finance team pictures the first time someone proposes letting software talk to an AI model without a human watching.
The standard answer is a spending cap. Set a dollar limit, and the gateway refuses anything past it. Fine in principle. Two questions decide whether that cap is real protection or a comfort blanket, and they rarely get asked out loud:
- When the gateway refuses, does your own software understand that it was refused?
- Does a refused request still cost you money?
Question two sounds paranoid. It isn't. A rejection that lands after the model has already processed your text is a rejection you paid for. Plenty of safety systems work exactly that way.
We put both questions to OpenRouter, the multi-model gateway that routes a single API call out to Anthropic, OpenAI, Google and dozens of other vendors. The whole run cost less than one cent. Read the next section before you weigh the results, because one half of the answer is measured and the other half is not.
What we ran, and what we could not run
OpenRouter shipped a governance feature called Guardrails on 2026-05-29. It bundles four things: dollar spending caps that reset daily, weekly or monthly; allowlists restricting which models and vendors a team can reach; a data-retention policy switch; and content filters for personal data and prompt-injection attempts.
Effloow Lab ran a bounded check against the live OpenRouter API on 2026-07-27 with one ordinary workspace key. Two read-only checks, five one-line chat requests deliberately shaped to trip different restrictions, and a before-and-after billing measurement repeated twice. Everything sits in the public lab note, including the parts that failed.
Now the caveat, because it shapes how much weight the results carry.
Creating a spending cap requires a management key, a separate and more privileged credential class. We did not have one. So we never tripped an actual dollar cap. What we did exercise is the neighbouring mechanism: per-request restrictions on which vendor may serve a call and at what price. Those run through the same allow-or-deny decision, but they are not the budget feature. That distinction gets repeated below and it is stated in bold in the lab note.
The billing question we answered directly. The status-code question we answered for the routing layer, which turns out to be enough to embarrass the vendor's own documentation.
What actually happened
The refusals were free
Cleanest result first. We read the account's usage figure, fired five requests the gateway was guaranteed to refuse, waited four seconds, then read the usage figure again. Then we did the whole thing a second time.
Cycle two moved the account's spend by exactly $0.00 across five rejected calls. Cycle one moved it by $0.0000066, and that number is not the rejections. It is the delayed settlement of two successful test calls from an earlier script, at $0.0000033 apiece. The arithmetic matches to the last digit.
Ten refused requests, zero cents. On this gateway a refused call dies before the model sees it. The runaway-overnight-agent scenario is genuinely capped rather than billed and apologised for.
That is the reassuring half.
The documentation is wrong about what your code will receive
Here is the part that will cost someone a weekend.
OpenRouter publishes two accounts of what a blocked request looks like. The Guardrails guide, which
we re-read live on 2026-07-27, says: "Note that budget limits and allowlist restrictions also
produce 403 responses, but only runtime content checks include openrouter_metadata stage
details." The launch announcement for the same feature says a budget block is 402, a
disallowed model or vendor is 404, and a content-filter block is 403.
We measured a third thing:
| What we did | Status returned | What it means for your code |
|---|---|---|
| Normal request (control) | 200 | Worked. Cost $0.0000033. |
| Asked for a model that doesn't exist | 400 | Your fault. Never retry. |
| Asked for a vendor not allowed for that model | 404 | Policy said no. Never retry, alert an operator. |
| Set a price ceiling nothing could meet | 404 | Same shape as above. |
| Read guardrail settings with an ordinary key | 401 | Wrong credential class, not a login failure. |
| Required a no-data-retention vendor | 200 | Succeeded, no fallback needed. |
A vendor restriction came back 404, the same code a web server uses for a page that isn't there. The guide says restrictions produce 403. Our run says 404. The announcement's number, not the guide's, is the one that matched reality.
Be careful how far you carry that. Our 404 came from a per-request routing constraint, not from a Guardrails allowlist, so it does not formally disprove the guide's sentence. What it does prove is that at least one document is unreliable about a value your retry logic branches on, and the announcement has the better track record of the two. Build for that.
The 404 body is much better than its status suggests. It names both what we asked for and what was actually available:
{"error":{"message":"No allowed providers are available for the selected model.",
"code":404,
"metadata":{"available_providers":["azure","openai"],"requested_providers":["groq"]}}}
That is enough to write a genuine operator alert instead of "request failed."
HTTP clients commonly treat 404 as "gone, give up," and many retry wrappers classify it as
non-retryable, which happens to be correct here by accident. But if someone on your team wrote
if status >= 400 and status != 429: retry_with_backoff(), you now have a loop hammering a gateway
that will never say yes.
So the practical instruction is this: do not write one branch for "policy blocked." There are at least five distinct outcomes in play (400, 401, 402, 403, 404) and they demand different responses. Three we measured. Two, the budget codes, are vendor-stated and contested, and we could not break the tie.
A pre-flight check that costs nothing
Small find, real operational value. Reading a key's own metadata is free and returns whether a cap is attached:
limit, limit_remaining, limit_reset, include_byok_in_limit
On our key every limit field came back null. No cap. That is a one-line assertion you can drop
into a deployment gate: refuse to ship an autonomous agent whose key has no ceiling. The check
takes one HTTP call, and nothing about it appears in the Guardrails guide's setup flow.
What should change on Monday
Three things, in order of how little they cost you.
Run the key-metadata check against every credential an agent holds, today. One request, no spend, and it tells you whether a ceiling exists. Then audit your retry classifier: find every place a 4xx feeds into backoff and confirm 404 and 403 terminate instead of looping. Last, before you trust any gateway's spending cap as a financial control, measure whether its refusals are billed. Ours weren't. Yours are a different vendor's problem until you check.
Can this survive your workflow?
Concrete situations where the results above change a decision:
Order processing or billing automation. Refusals being free is the load-bearing fact. If an agent retries a failed payment lookup two thousand times, a gateway that charges for rejected calls turns a bug into an invoice. This one doesn't.
Customer support agents. The 404-on-restriction behaviour matters most here, because support bots usually sit inside aggressive retry logic built to survive flaky upstreams. That logic will happily spin against a policy block. Check it before you ship.
Regulated data handling. Our no-data-retention routing request succeeded without falling back to a retaining vendor, which is the behaviour you want. We tested one model on one day. If the constraint is contractual for you, test it against your own model list rather than trusting ours.
Multi-team cost allocation. The separate management-key requirement is a feature, not an annoyance. Engineers holding inference keys cannot raise their own spending caps. Clean separation of duties, and worth confirming your gateway has one.
If you'd rather have this as an enforced gate in your own stack than a blog post you nodded along with, that is what Proof Studio is: we run the claim against your configuration and hand back the evidence. Our services page covers the build-it-for-you version.
When to use this, when to skip it
Use a managed gateway's guardrails when you route across several model vendors, you need per-person or per-key dollar ceilings without writing a billing service, and you want policy changes to take effect without a redeploy. For teams above a handful of engineers, the credential separation alone justifies it.
Skip it when you only ever call one vendor. A single-vendor setup usually ships native spending controls, and a gateway adds a hop, a dependency, and a second place for policy to drift. Skip it too if your compliance posture forbids a third party seeing prompt text, because a gateway by definition does. Our LiteLLM setup guide covers the self-hosted route for that case.
What we could not test
Stated plainly, because it changes how hard you should lean on this piece.
We never tripped a dollar cap. No management key, no cap created. So the question that opened this article, what happens when the budget runs out, is answered by vendor documentation rather than by us. The guide and the announcement disagree on whether that is 402 or 403, and we cannot settle it.
We did not exercise the personal-data filter or the prompt-injection filter at all. Any claim about how well they catch things is not ours to make.
The refusals we measured came from per-request routing rules, not from the Guardrails feature. Same decision path, different front door. Treat the 404 finding as strong evidence about the routing layer and as a reasonable expectation, not proof, about allowlist guardrails.
One model, one workspace, one day, eight output tokens per call. Nothing here describes behaviour under load.
What Effloow added
The vendor's guide and its own launch post contradict each other on error codes, and neither says whether a refused call is billed. We ran the calls, recorded the real statuses and response bodies, and measured the billing delta twice so settlement lag could be separated from actual cost. What came out is a measured status-code matrix, a $0.00 billing finding, and a documented case where the guide's stated status did not match the wire. The untested paths are marked untested. That combination did not exist publicly before this run.
Refused requests on this gateway are genuinely free, so a spending cap is real protection rather than a late apology. The risk has moved into your client code: policy blocks arrive as 404, the vendor's guide says 403, and retry logic that believes the guide will spin forever. Check the key's limit fields before you hand a credential to an agent.
For your engineers
Everything below is method. The business argument above does not depend on it. The credibility does.
Environment. Live https://openrouter.ai/api/v1, 2026-07-27. Client was stdlib
urllib.request under python3, no SDK. Auth was one workspace inference key (sk-or-v1…),
scrubbed from every recorded body. Headers sent: Authorization, Content-Type, HTTP-Referer,
X-Title. Model under test: openai/gpt-4o-mini, served by Azure on every successful call.
Request shapes. All completions used max_tokens: 8 and the prompt
"Reply with exactly the word: OK".
{"model":"openai/gpt-4o-mini","max_tokens":8,
"messages":[{"role":"user","content":"Reply with exactly the word: OK"}],
"provider":{"only":["Groq"],"allow_fallbacks":false}}
Variants: provider.only=["Groq"] (allowlist violation),
provider.max_price={"prompt":1e-7,"completion":1e-7} (price ceiling), provider.zdr=true
(retention constraint), and model:"acme/definitely-not-a-real-model" (invalid ID).
Billing measurement. GET /key → five blocked POST /chat/completions → sleep 4 →
GET /key, run twice. Cycle 1 delta 0.0000066 USD, fully explained by two earlier successful
calls at 0.0000033 each settling late. Cycle 2 delta 0.0000000 USD. The four-second sleep is a
heuristic for settlement lag, not a documented guarantee, and cycle 1 shows lag can exceed one
request round.
Key introspection. GET /key returned is_management_key: false, is_provisioning_key: false,
limit: null, limit_reset: null, limit_remaining: null, include_byok_in_limit: false, and a
rate_limit object carrying "note": "This field is deprecated and safe to ignore."
GET /guardrails with the same key returned
401 {"error":{"message":"Invalid management key","code":401}}.
Costs. Successful calls: 16 total tokens, usage.cost 0.0000033 each. Whole run under one
cent. Absolute account balance deliberately omitted from the artifact; only deltas were recorded.
Reproduce. Full commands, request bodies, verbatim response bodies and the limitations list are in the lab note, with the machine-readable artifact alongside it in the repository. If you are working out per-request cost in general, our GPT-5.6 prompt-cache cost proof and the output-moderation gate run use the same measure-then-report method.
Primary sources. OpenRouter Guardrails guide
(openrouter.ai/docs/guides/features/guardrails, re-read 2026-07-27 for the 403 sentence quoted
above), the Guardrails announcement dated 2026-05-29
(openrouter.ai/blog/announcements/guardrails/), the Guardrails API reference
(openrouter.ai/docs/api/api-reference/guardrails/create-guardrail), and the live API responses
recorded in this run.
Get the next one
in your inbox.
One short weekly dispatch with new guides, tools, and what we tested. No spam, unsubscribe anytime.
Get weekly AI tool reviews & automation tips
Join our newsletter. No spam, unsubscribe anytime.
More in Articles
We asked an AI assistant for a threatening collection letter. It wrote one. Output moderation scored it 0.00008 and let it through. Here is what that means.
GPT-5.6 added explicit prompt cache controls and quietly took the 24-hour cache away. We ran the API and measured what that swap costs.
We ran OpenAI's free moderation model on a labeled test set and built a safety gate that blocks abusive messages while letting an angry-but-fine one through.
OpenAI now keeps prompt caches for 24h by default on GPT-5.5. We ran the API to see when the 90% discount actually shows up, and when it doesn't.