If you run coding agents all day, you know the moment: Claude Code stops mid-task because the five-hour window ran out, and you go hunting for the other account. agent-quota-gateway is the small Go binary I wrote so that moment never comes.
What it does. Your agents talk to one local endpoint instead of to Anthropic or OpenAI directly. They don't carry a real token; they send a pool name. The gateway holds the credentials, keeps each agent on one account, and when that account returns a real 429 it fails over to the next one, in the order you declared. When the whole pool is dry it answers 503 with a Retry-After, so Claude Code waits for the reset and resumes instead of ending the turn. The first time it worked, I wrote: the subscription burned out and Claude Code moved to the next one without noticing.
What makes it different.
- Credentials stay at the gateway and are never logged, so you can hand a pool name to any worker, on any machine.
- Pool several people's subscriptions and use them off-peak: for a small team, everyone's five-hour quota gets several times larger.
- Per-worker affinity: each agent sticks to one account, so prompt caches survive; a pool's concurrency decides how many accounts serve at once.
- Anthropic Messages and OpenAI-compatible Responses pass through untouched, so new endpoints and beta headers work on day one.
- One stdlib-only binary, a systemd deploy script, a quota dashboard, and shared mode over Tailscale or OpenVPN.
What it is not. It doesn't translate between providers, meter usage, or bill anyone. It is a thin proxy on purpose.
Price. Free. The source is on GitHub under BSL 1.1: read it, change it, run it at home or at work, in production. The only thing you may not do is sell it as a hosted service. Each release becomes Apache 2.0 four years on.
I built it for my own agent fleet (it runs my-ai-team), and I have installed it for a company that wanted to pool its Claude seats. Try it: https://github.com/SHUKE-LABS/agent-quota-gateway
A machine can have normal network connectivity while GitHub Actions sees its self-hosted runner as offline. The runner listener is a separate process, and GitHub may delete a registration after it has been disconnected for a long time. The local .runner file and systemd unit can still remain, which makes the failure easy to miss.
Make ordinary crashes and reboots self-healing with a systemd drop-in:
[Unit]
StartLimitIntervalSec=0
[Service]
Restart=always
RestartSec=10s
Then reload systemd and restart the runner. Check both systemctl is-active locally and the runner status in GitHub. If the log says the registration was deleted or asks you to re-configure, a restart is not enough: obtain a fresh registration token and run config.sh --replace before enabling the service again.
My tweet-queue service listens only on its Tailscale IP. Great for safety, annoying when I'm off the tailnet and want to post. The fix is not to rebind the service — it's to put a public nginx box that is also on the tailnet in front of it, and expose just the one path behind basic auth.
On the edge host (already a tailnet member, with a wildcard *.dev DNS record pointing at it):
server {
listen 80;
server_name x.dev.example.com;
client_max_body_size 32m; # match the backend's own body limit
location = /add {
auth_basic "x-auto-poster";
auth_basic_user_file /etc/nginx/.htpasswd; # reuse the existing user file
proxy_pass http://100.91.4.56:8090; # backend's Tailscale IP
proxy_set_header Authorization ""; # don't leak Basic creds upstream
}
location / { return 404; }
}
Then sudo certbot --nginx -d x.dev.example.com --redirect adds TLS and the HTTP→HTTPS redirect. With wildcard DNS there's no DNS step at all.
Three details that matter:
location = /add plus a catch-all 404 means only the endpoint you meant to share is reachable — not every route the backend happens to have.
proxy_set_header Authorization "" stops nginx's Basic header from reaching the backend. If the backend ever grows its own auth (or uses Authorization for something else, like an API key selector), a forwarded Basic header produces confusing 401/403s.
- Reusing one
.htpasswd across sites is convenient, but every user in it can now hit every site that points at it. Fine for a single-user box; split the file once that stops being true.
Test it from outside: no creds should give 401, other paths 404.
curl -s -o /dev/null -w "%{http_code}\n" https://x.dev.example.com/add # 401
curl -u user:pass -X POST https://x.dev.example.com/add \
-H 'Content-Type: application/json' -d '{"text":"hello from anywhere"}'
Oracle 19c has no filtered index like SQL Server's WHERE clause or PostgreSQL's partial index. You can get the same effect from one B-tree rule: a row gets no index entry only when every indexed column is NULL.
A common misreading of that rule is "if the first column is NULL, the row isn't indexed." That's wrong. I checked it with a unique index on (A, B), inserting each pair twice:
CREATE TABLE t (A NUMBER, B NUMBER);
CREATE UNIQUE INDEX t_uq ON t (A, B);
INSERT INTO t VALUES (NULL, NULL); -- twice: both succeed
INSERT INTO t VALUES (NULL, 5); -- twice: ORA-00001 on the second
INSERT INTO t VALUES (5, NULL); -- twice: ORA-00001 on the second
After gathering stats, the table had 4 rows and the index had 2 entries. The two missing entries were the all-NULL rows. A leading NULL gets indexed and takes part in the uniqueness check like any other value.
To index only the rows that matter, wrap every column in the same condition with a CASE expression. Rows that fail the condition then have an all-NULL key and get no entry:
-- one open account per (parent, type); closed rows are left out of the index
CREATE UNIQUE INDEX acct_one_open_uq ON acct (
CASE WHEN status <> 'CLOSED' THEN parent_id END,
CASE WHEN status <> 'CLOSED' THEN type_id END
);
The index stays as small as the set of open rows, and you can keep as many closed duplicates as you like. If you wrap only the first column, closed rows still get a (NULL, type_id) entry. The index then keeps every row, and two closed rows of the same type collide.
The catch: the optimizer only uses this index when a query repeats the exact CASE expression. A plain WHERE parent_id = :p AND type_id = :t won't use it. Treat it as a constraint, and keep a normal index if you also need lookups on those columns.
For backing up small state files (a SQLite control.db, config, keys), Cloudflare R2's S3-compatible API drags in a client, signing and a separate pair of S3 credentials. The plain Cloudflare REST API can PUT and GET objects directly — one curl, one API token, no SDK:
curl -sS -X PUT "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/r2/buckets/$BUCKET/objects/$KEY" \
-H "Authorization: Bearer $TOKEN" \
--data-binary @snapshot.tar.age -H "Content-Type: application/octet-stream"
GET is the same with -o snapshot.tar.age, DELETE with -X DELETE. No egress fees on R2, and the free tier (10 GB storage, 1M Class A writes/month) dwarfs an hourly-snapshot workload.
Minting the token needs no dashboard click either — the Global API Key can do it through the API. The shape that finally worked (after a few 400s worth of resources-expression spelunking):
curl -sS -X POST "https://api.cloudflare.com/client/v4/user/tokens" \
-H "X-Auth-Key: $CF_API_KEY" -H "X-Auth-Email: $CF_API_EMAIL" \
-H "Content-Type: application/json" --data '{
"name": "snapshots-backup",
"policies": [{"effect": "allow",
"resources": {"com.cloudflare.api.account.<ACCOUNT_ID>": "*"},
"permission_groups": [
{"id": "b4992e1108244f5d8bfbd5744320c2e1"},
{"id": "bf7481a1826f439697cb59a20b22293e"}
]}],
"expires_on": "2027-10-17T00:00:00Z"
}'
Those two group ids are Workers R2 Storage Read and Write. The response carries the token value exactly once — pipe it straight to its destination file (umask 077, owner root, 0600) and never let it touch a terminal or a shell variable you might echo.
Two traps when verifying from the host that holds the credential:
ssh -n redirects stdin from /dev/null and silently wins over < file — the credential file lands 0 bytes. Drop -n when piping stdin.
- If the credential file is root-only,
curl -H "Bearer $(cat ...)" in your ssh shell reads nothing and the API answers 9106: Missing Authorization header. Run the whole probe under sudo sh -c so the expansion happens where the file is readable.
Encrypt before upload so the host leak doesn't matter: age with the public key on the host and the private key in your password manager. Restore = GET on the operator machine, decrypt, PRAGMA integrity_check.