A month ago during vacation I wrote that I finally had time to think through AI-SDLC and maybe go to open source if it is useful for other people. This is the first part of it: Yabanin Core, the gateway I use between my coding agents and the models they call. It is on GitHub now under Apache-2.0.
Coming from data platforms I find this part of the agent conversation familiar. Everybody talks about the model, and then it turns out the rest of the system needs a lot of attention too. In one of my pet-project repositories I counted 68 worktree checkouts after one day of development (some were retained from earlier tasks, so it is an accumulated inventory and not 68 agents running at once). Agents create a lot of parallel work, all of it spends tokens, and in the evening I could not answer simple questions:
- which models was each agent allowed to use?
- how much could it spend before somebody noticed?
- what did a task or a PR actually cost?
With one shared provider key there is no way to answer them, and this is why I put a gateway in between.
What Yabanin Core is
Yabanin Core is a self-hosted gateway. Agents and apps connect to it through OpenAI- or Anthropic-compatible endpoints (for most clients it is just a base URL change), and it sends requests to hosted providers, your own servers or local models. It routes requests, retries and falls back to other models, caches responses and logs every request. There is also a dashboard, an admin API and a yb CLI.
Most of this you can find in other gateways too. What I wanted in addition is that an agent is a separate identity with its own limits, and below are three decisions I made around it.
Quickstart
You do not need a provider account because a mock provider answers the requests, so nothing costs money. You need Docker with Compose v2, Git, Bash, OpenSSL, curl and Python 3.
git clone https://github.com/yabaninai/yabanin-core.git cd yabanin-core bash scripts/quickstart/demo.sh The script builds and starts the gateway, Postgres, Redis, the mock provider and the dashboard, and at the end prints an API key and an admin password. They are shown only once, so save them. Put the key into YABANIN_API_KEY and send a request:
curl http://127.0.0.1:8082/v1/chat/completions \ -H "Authorization: Bearer $YABANIN_API_KEY" \ -H 'Content-Type: application/json' \ --data '{"model":"mock-model","messages":[{"role":"user","content":"Hello from Yabanin"}]}' The dashboard is on http://127.0.0.1:3000 (user admin@localhost), and everything listens only on 127.0.0.1. The first build takes a few minutes, after that the stack starts in about ten seconds.
Budgets for agent keys
When a person sends prompts by hand, a key without a limit is usually fine. An agent can loop all night, so for agent keys I made it the other way around, and an agent without a configured budget cannot spend at all.
| Situation | Response | Error code |
|---|---|---|
| No budget configured for this agent | 403 | agent_budget_not_configured |
| Daily limit reached | 429 | agent_budget_exceeded |
| Budget store unreachable | 503 | agent_budget_check_unavailable |
For me the most important case is the last one. If the budget store is not available, the gateway refuses the request instead of letting it through. Simplified from pkg/gateway/admission.go:
switch { case errors.Is(err, budget.ErrNoAgentBudget): // 403 agent_budget_not_configured case errors.Is(err, budget.ErrAgentBudgetExceeded): // 429 agent_budget_exceeded default: // 503 agent_budget_check_unavailable: never "allow" when we can't check } To give an agent a key and a budget you need two calls to the admin API:
POST /api/agent-keys {"name": "nightly-etl", "org_id": "<org>"} PUT /api/agent-keys/<id>/budget {"daily_limit_cents": 500} There is one limitation you should know about. By default the budget is checked before each request against the spend that is already recorded, and spend is recorded after the response, so a request that is already running can finish over the limit. If you need a hard cap, there is an opt-in reservation mode which books the estimated cost before the call.
This is less comfortable on the first day. For myself it is the same story as with CI/CD and linters: the parts that are checkable should be enforced, otherwise people end up supervising every agent step manually.
Child keys for sub-agents
Agents hand work to other agents, and if a sub-agent gets the parent's key it also gets the whole budget and all the tools. Instead an operator can mint a child key for the sub-task:
POST /api/agent-keys/delegate {"parent_agent": "<agent-key id>", "org_id": "<org>", "name": "subtask-render", "ttl": 86400, "budget_caps": 500, "mcp_grants": ["knowledge"]} When the key is minted, the TTL is required and can be at most 90 days. The child's cap has to fit into what the parent has left today, which is its daily limit minus its own spend minus the caps of its other active children. This check runs under a lock, so two delegations at the same time cannot both pass. MCP grants are intersected with the parent's grants, so a child never gets access to a server the parent does not have, and a child cannot delegate further.
After that, on every request the parent's limit is checked against its own spend plus the spend of its children. So several sub-agents spend from one parent budget and do not get a separate budget each.
Cost per task and per PR
Some time ago I wrote about what it costs an AI agent to finish the job [link to the earlier article]. Tokens are only one part of that cost, but this part can be measured if requests are tagged. In agentic development one task usually ends up as one PR, so I put the same tag on both sides. Commits get a trailer:
git commit -m "feat: add login" -m "Yabanin-Task: a142-add-login" Model requests get the same value in the X-Yabanin-Task header (or as a label on the key). After that spend is grouped by task in the dashboard and in GET /api/analytics/tasks, with CSV export, and you can also group it by session or by agent.
Every response also carries the routing decision in a header:
X-Yabanin-Decision: {"source":"tier_router","class":"easy","confidence":0.92,"threshold":0.7,"from":"gpt-4o","to":"gpt-4o-mini","reason":"time_window_downgrade"} So when a request goes to a model you did not expect, you can see the reason.
Where it runs
| Setup | How | Good for |
|---|---|---|
| Laptop | Docker Compose, ports on 127.0.0.1 only | Trying it, local models |
| Your cloud or servers | Kubernetes with Helm, routes to the providers you configure | Teams |
| Air-gapped | Offline bundle: verify checksums, the cosign signature, the SBOM and image digests, then docker load or Helm | Regulated or offline environments |
Self-hosting the gateway does not make inference local. If you route to a hosted provider your prompts still go there, and for a fully local setup you need a local model (for example llama.cpp) behind the gateway.
Limitations
It is a Developer Preview, so a few limitations:
- the release kit is Linux amd64 only for now
- budgets are only as good as your prices. The bundled reference prices cover a limited set of models and may be out of date, so set your own before you trust the cost numbers
- the guard hooks for secrets, PII and prompt injection are regex and checksum detectors. They will catch obvious cases, but this is not a DLP system
- the release notes list two open findings from the dependency vulnerability scan, details are in
SECURITY.md
Next steps
Today the gateway covers boundaries and visibility. In the next versions I want to add richer policy rules and detection of agent behaviour that looks wrong and not only expensive (this is a plan, it is not in the code yet).
If you try it, please open an issue when something breaks, it is much better to find problems now than in six months. As I wrote before, when you saved 80 hours of coding you still have to spend 1000 hours of support. I would also like to hear if deny-by-default is the right call or just annoying, I am not sure yet where the line should be. PRs are welcome, details are in CONTRIBUTING.md.
Own your AI, even if you depend on cloud models right now :)
I used AI to help edit the text of this post.


