Get 175 USD in Free API Credits with AgentRouter (and Use Them in OpenCode)
Complete guide to claiming 175 USD in free AgentRouter API credits, accessing GPT-5.6 Sol, Claude Opus 5 and Opus 4.8, and wiring them into OpenCode with a Python proxy that fixes the 401 errors.

Get 175 USD in Free API Credits with AgentRouter (and Use Them in OpenCode)
API credits are the real bottleneck when you work with coding agents. Between Claude Opus, GPT-5 and the endless round trips of an agent refactoring your project, the bill adds up fast.
AgentRouter is currently offering 175 USD in free credits at signup. In this article I'll walk through how to claim them, then how to wire everything into OpenCode — including the part nobody documents: the 401 unauthorized client detected errors that stop most people on their first launch.
Disclosure: the signup link below is an affiliate link. It's also the condition for unlocking the 175 USD — without going through a referral link, the credits never land on the account.
What AgentRouter Is
AgentRouter is an OpenAI-compatible API router. In practice: one key, one base URL, and behind it you reach several models from different providers. Because the API follows the OpenAI format, any tool that can talk to OpenAI can talk to AgentRouter — with a few adjustments we'll get into.
Available Models
Three models are accessible:
- gpt-5.6-sol
- claude-opus-5
- claude-opus-4-8
Short list, but these are exactly the models that matter for agentic coding. Nothing wasted.
Step 1 — Create the Account and Claim the 175 USD
There's a single requirement:
Your GitHub account must have been created before 2025.
That's the anti-abuse guardrail. A GitHub account made this morning will be rejected, so don't bother trying.
To sign up:
- Go to agentrouter.org
- Sign in with GitHub
- Credits are added to the account automatically
- Generate an API key from the dashboard
Keep that key handy, we need it right away. And never commit it to a Git repository.
Step 2 — Why a Proxy Is Necessary
If you configure OpenCode to hit https://agentrouter.org/v1 directly, you'll run into one of these walls:
401 unauthorized client detectedJSON parsing failedUnexpected non-whitespace character after JSON- A model that spins forever without ever responding
These aren't mistakes in your config. There are three genuine incompatibilities between the OpenCode SDK and AgentRouter:
1. The User-Agent
AgentRouter only authorizes requests carrying the header User-Agent: opencode/<version>. The OpenCode AI SDK sends its own and gets rejected with a 401 — even with a perfectly valid key. This is the number one cause of "invalid key" messages when the key is actually fine.
2. Null Fields in the Request
The SDK sends fields set to null, typically response_format: null. AgentRouter refuses and returns:
3. billing.summary Objects in the Response
AgentRouter injects billing objects into the middle of the SSE stream:
That's neither a completion chunk nor an error. The SDK's Zod validator doesn't know what to do with it and parsing breaks. Same problem with data: null events.
A small local proxy solves all three at once: it rewrites the header, cleans the outgoing request and filters the incoming stream.
Step 3 — Install the Proxy
First check that Python 3.10 or newer is installed:
You can download the script directly, or copy it from the block below — same file either way.
↓agentrouter-proxy.pyPython script · 306 lines · no dependencies
Drop it in any folder you like. If you'd rather create it by hand, make a file named agentrouter-proxy.py and paste this script into it:
#!/usr/bin/env python3
"""
AgentRouter <-> OpenCode proxy (OpenAI-compatible endpoints).
Fixes three incompatibilities:
1. Injects the User-Agent required by AgentRouter (avoids 401).
2. Strips null fields from the request body (avoids 400).
3. Filters billing.summary and `data: null` events from the response.
Usage:
export AGENTROUTER_API_KEY="sk-..."
python3 agentrouter-proxy.py [port] # default port: 4182
Environment variables:
AGENTROUTER_API_KEY AgentRouter API key (required)
AGENTROUTER_UPSTREAM Upstream URL (default: https://agentrouter.org)
AGENTROUTER_DEBUG=1 Dump requests and SSE events to stderr
"""
import http.client
import http.server
import json
import os
import ssl
import sys
import threading
import urllib.error
import urllib.request
UPSTREAM = os.environ.get("AGENTROUTER_UPSTREAM", "https://agentrouter.org")
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 4182
API_KEY = os.environ.get("AGENTROUTER_API_KEY", "")
DEBUG = os.environ.get("AGENTROUTER_DEBUG", "") == "1"
# Without this exact User-Agent, AgentRouter returns 401 even with a valid key.
USER_AGENT = "opencode/1.17.12"
CTX = ssl.create_default_context()
def strip_nulls(value):
"""Recursively remove dict entries whose value is None."""
if isinstance(value, dict):
return {k: strip_nulls(v) for k, v in value.items() if v is not None}
if isinstance(value, list):
return [strip_nulls(item) for item in value]
return value
def clean_request_body(raw: bytes) -> bytes:
"""Drop null fields from the outgoing JSON body."""
try:
payload = json.loads(raw)
except (json.JSONDecodeError, ValueError):
return raw
if not isinstance(payload, dict):
return raw
return json.dumps(strip_nulls(payload), separators=(",", ":")).encode()
def is_billing_summary(payload) -> bool:
"""AgentRouter injects billing.summary objects the AI SDK cannot parse."""
return isinstance(payload, dict) and payload.get("object") == "billing.summary"
def clean_sse_event(event: bytes):
"""Filter a single SSE event. Return None to drop it."""
stripped = event.strip()
if not stripped:
return event # SSE separator, keep as-is
if stripped.startswith(b"data:"):
payload = stripped[len(b"data:"):].strip()
if payload in (b"", b"null"):
return None
try:
parsed = json.loads(payload)
except (json.JSONDecodeError, ValueError):
return event # e.g. "[DONE]"
return None if is_billing_summary(parsed) else event
# Bare JSON with no `data:` prefix
if stripped[:1] in (b"{", b"["):
try:
parsed = json.loads(stripped)
except (json.JSONDecodeError, ValueError):
return event
if is_billing_summary(parsed):
return None
return event
def clean_nonstream_body(raw: bytes) -> bytes:
"""Replace a billing.summary response with a readable JSON error."""
if not raw:
return raw
try:
payload = json.loads(raw)
except (json.JSONDecodeError, ValueError):
return raw
if is_billing_summary(payload):
sys.stderr.write("[ar] filtered billing.summary from non-stream response\n")
return json.dumps(
{
"error": {
"message": "agentrouter billing.summary is not a chat response",
"type": "proxy_filtered",
}
}
).encode()
return raw
class ProxyHandler(http.server.BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_POST(self):
self._proxy()
def do_GET(self):
self._proxy()
def do_OPTIONS(self):
self.send_response(200)
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "*")
self.send_header("Content-Length", "0")
self.end_headers()
def _proxy(self):
content_len = int(self.headers.get("Content-Length", 0))
raw_body = self.rfile.read(content_len) if content_len > 0 else None
if DEBUG and raw_body is not None:
try:
payload = json.loads(raw_body)
sys.stderr.write(
f"[ar-dbg] req model={payload.get('model')} "
f"stream={payload.get('stream')} "
f"messages={len(payload.get('messages', []))} "
f"bytes={len(raw_body)} keys={list(payload.keys())}\n"
)
except (json.JSONDecodeError, ValueError):
sys.stderr.write(f"[ar-dbg] req {len(raw_body)}b (not JSON)\n")
body = clean_request_body(raw_body) if raw_body else None
headers = {
k: v
for k, v in self.headers.items()
if k.lower()
not in ("host", "connection", "transfer-encoding", "accept-encoding")
}
headers["Authorization"] = f"Bearer {API_KEY}"
headers["User-Agent"] = USER_AGENT
# Force an uncompressed response: gzip/br cannot be filtered line by line.
headers["Accept-Encoding"] = "identity"
if body is not None:
headers["Content-Length"] = str(len(body))
url = f"{UPSTREAM}{self.path}"
request = urllib.request.Request(
url, data=body, headers=headers, method=self.command
)
is_stream = bool(body) and b'"stream":true' in body.replace(b" ", b"")
try:
response = urllib.request.urlopen(request, context=CTX, timeout=300)
if is_stream:
self.send_response(response.status)
for k, v in response.headers.items():
if k.lower() not in (
"transfer-encoding",
"connection",
"content-length",
):
self.send_header(k, v)
self.send_header("Transfer-Encoding", "chunked")
self.end_headers()
self._stream_sse(response)
else:
try:
raw_response = response.read()
except http.client.IncompleteRead as exc:
raw_response = exc.partial
payload = clean_nonstream_body(raw_response)
self.send_response(response.status)
for k, v in response.headers.items():
if k.lower() not in (
"transfer-encoding",
"connection",
"content-length",
):
self.send_header(k, v)
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
except urllib.error.HTTPError as exc:
err_body = exc.read()
sys.stderr.write(
f"[ar] ERROR {exc.code}: {err_body.decode(errors='replace')[:500]}\n"
)
self.send_response(exc.code)
self.send_header(
"Content-Type", exc.headers.get("Content-Type", "application/json")
)
self.send_header("Content-Length", str(len(err_body)))
self.end_headers()
self.wfile.write(err_body)
except Exception as exc: # noqa: BLE001 - proxy must never crash
sys.stderr.write(f"[ar] EXCEPTION: {exc}\n")
payload = json.dumps({"error": {"message": str(exc)}}).encode()
self.send_response(502)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def _stream_sse(self, response):
"""Forward the SSE stream, dropping `data: null` and billing.summary."""
buffer = b""
try:
while True:
try:
chunk = response.read(1)
except http.client.IncompleteRead as exc:
chunk = exc.partial
if not chunk:
break
buffer += chunk
if not (buffer.endswith(b"\n\n") or buffer.endswith(b"\r\n\r\n")):
continue
if DEBUG:
sys.stderr.write(
f"[ar-dbg] event ({len(buffer)}b): {buffer[:400]!r}\n"
)
cleaned = clean_sse_event(buffer)
buffer = b""
if cleaned is None:
if DEBUG:
sys.stderr.write("[ar-dbg] -> dropped by filter\n")
continue
self._write_chunk(cleaned)
if buffer:
cleaned = clean_sse_event(buffer)
if cleaned is not None:
self._write_chunk(cleaned)
self.wfile.write(b"0\r\n\r\n")
self.wfile.flush()
except (ConnectionResetError, BrokenPipeError):
return
def _write_chunk(self, data: bytes):
self.wfile.write(f"{len(data):X}\r\n".encode())
self.wfile.write(data)
self.wfile.write(b"\r\n")
self.wfile.flush()
def log_message(self, fmt, *args):
sys.stderr.write(f"[ar] {args[0]}\n")
class ThreadedHTTPServer(http.server.HTTPServer):
"""Handle each request in its own thread so streaming never blocks."""
daemon_threads = True
def process_request(self, request, client_address):
thread = threading.Thread(
target=self.process_request_thread, args=(request, client_address)
)
thread.daemon = True
thread.start()
def process_request_thread(self, request, client_address):
try:
self.finish_request(request, client_address)
except Exception: # noqa: BLE001
self.handle_error(request, client_address)
finally:
self.shutdown_request(request)
if __name__ == "__main__":
if not API_KEY:
print("ERROR: set AGENTROUTER_API_KEY first", file=sys.stderr)
sys.exit(1)
server = ThreadedHTTPServer(("127.0.0.1", PORT), ProxyHandler)
print(f"agentrouter-proxy :{PORT} -> {UPSTREAM}")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nProxy stopped.")
server.server_close()
Nothing to install: everything comes from the Python standard library. No pip install, no virtual environment.
Step 4 — Set Your API Key
The key is passed through an environment variable, never hardcoded in the script.
macOS / Linux
Windows (PowerShell)
Windows (Command Prompt)
Be aware: these commands only apply to the current terminal. Close the window and you start over. To make it permanent on macOS or Linux, add the export line to your ~/.zshrc or ~/.bashrc.
Step 5 — Start the Proxy
Open a terminal in the folder containing agentrouter-proxy.py, then run:
You should see:
Keep this terminal open. The proxy has to keep running the whole time you use OpenCode. Close it and OpenCode can no longer send anything.
The default port is 4182. If it's already taken on your machine, pass a different one as an argument:
Just remember to use the same port in the config in the next step.
Step 6 — Configure OpenCode
Open your OpenCode configuration folder and create opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"provider": {
"agentrouter": {
"name": "AgentRouter",
"npm": "@ai-sdk/openai-compatible",
"options": {
"baseURL": "http://127.0.0.1:4182/v1",
"apiKey": "YOUR_AGENTROUTER_API_KEY"
},
"models": {
"gpt-5.6-sol": { "name": "GPT 5.6 Sol" },
"claude-opus-5": { "name": "Claude Opus 5" },
"claude-opus-4-8": { "name": "Claude Opus 4.8" }
}
}
}
}
Replace YOUR_AGENTROUTER_API_KEY with your actual key.
The critical detail: baseURL points to http://127.0.0.1:4182/v1, meaning your local proxy — definitely not https://agentrouter.org/v1 directly. The whole setup depends on this.
Restart OpenCode, pick an AgentRouter model, and you're running. No additional configuration required.
Using the API Directly (Without OpenCode)
The proxy only exists to work around the OpenCode SDK's constraints. If you're writing your own code, you can hit AgentRouter directly.
With curl
With Python
Since the API is OpenAI-compatible, the official SDK works as-is:
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["AGENTROUTER_API_KEY"],
base_url="https://agentrouter.org/v1",
)
response = client.chat.completions.create(
model="claude-opus-5",
messages=[
{"role": "user", "content": "Explain closures in JavaScript"}
],
)
print(response.choices[0].message.content)
With JavaScript / TypeScript
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.AGENTROUTER_API_KEY,
baseURL: "https://agentrouter.org/v1",
});
const response = await client.chat.completions.create({
model: "gpt-5.6-sol",
messages: [
{ role: "user", content: "Explain closures in JavaScript" },
],
});
console.log(response.choices[0].message.content);
To switch models, change the model string. Nothing else moves.
Reading the Proxy Logs
The terminal running the proxy is your best diagnostic tool. Every time OpenCode makes a request, you'll see:
When something breaks, the proxy prints the real error coming back from AgentRouter:
or
That's far more actionable than the generic message OpenCode displays. To dig deeper, enable debug mode, which dumps requests and every SSE event:
Troubleshooting Common Issues
401 unauthorized client detected
The request isn't using the right User-Agent. Verify the proxy is running, that baseURL is http://127.0.0.1:4182/v1, and that you aren't pointing OpenCode straight at https://agentrouter.org/v1.
If it persists while the proxy is running, AgentRouter may have tightened its version check. Adjust the USER_AGENT constant in the script to match your installed OpenCode version.
JSON parsing failed / Unexpected non-whitespace character after JSON
Malformed SSE stream, almost always a billing.summary or a data: null. The proxy filters both — make sure requests actually go through it rather than direct.
Connection refused on 127.0.0.1:4182
The proxy isn't running. Start it again:
If the port is occupied by another program, pick a different one and update baseURL accordingly.
No AI Response
Check the proxy terminal first. If requests arrive but nothing comes back, the problem is upstream: either AgentRouter or the selected model. Try a different model to narrow it down.
Credits Not Showing Up
Two likely causes: your GitHub account is from 2025 or later, or you didn't sign up through a referral link. Without an affiliate link, the 175 USD never gets credited.
File Layout
Nothing is enforced, but this structure works well:
The proxy can live anywhere on your disk. The only link between the two is the port declared in baseURL.
Wrapping Up
Five minutes of setup for 175 USD in credits across three excellent models. The proxy looks intimidating at first, but it only does three things: rewrite a header, strip null fields and filter two kinds of junk events. Once it's running, you forget about it.
One last reminder that saves hours of debugging: the proxy has to stay open. Nearly every "it worked yesterday" comes down to a terminal closed between sessions.
Building something that needs AI agents, automation or a custom API integration? Let's talk.
Happy coding.
Website, photo shoot or video.
Let's talk.