When you shift inference traffic from one gateway to another, the boring accounting matters. Reconciling OpenRouter to n4n credit balances ensures you don’t leave paid capacity stranded or misread your new metering. This how-to gives exact steps to export balances, assess refund options, and verify token-level deduction on the destination.
Step 1: Pull your OpenRouter credit state via API
OpenRouter exposes a credits endpoint that returns your total purchased, used, and remaining balance. Use it instead of scraping the dashboard.
curl -H "Authorization: Bearer $OPENROUTER_KEY" \
https://openrouter.ai/api/v1/credits
The response shape is stable:
{
"data": {
"total_credits": 50.0,
"used_credits": 12.37,
"remaining_credits": 37.63
}
}
In Python, wrap it so you can log the snapshot to your migration ledger:
import os, requests, json
def openrouter_balance(key: str) -> dict:
r = requests.get("https://openrouter.ai/api/v1/credits",
headers={"Authorization": f"Bearer {key}"})
r.raise_for_status()
return r.json()["data"]
if __name__ == "__main__":
bal = openrouter_balance(os.environ["OPENROUTER_KEY"])
print(json.dumps(bal, indent=2))
Verification: you get HTTP 200 and a remaining_credits float. Record this number with a timestamp. The OpenRouter to n4n credit balances comparison starts with this figure.
Step 2: Review OpenRouter refund policy and eligibility
OpenRouter’s terms treat credits as a prepaid utility. Used credits are not refundable. Unused credits may be refunded at their support team’s discretion, typically via a manual ticket rather than self-service.
Do not write code for this. Open a support conversation with your account email, state the remaining balance from Step 1, and ask for the refund path. If they decline, plan to drain the balance (Step 6).
Engineers often miss that promotional credits granted by OpenRouter are separate from purchased credits and are almost never refundable. Inspect the total_credits composition if you have historical records; the API does not split them, so you need your billing email trail.
Step 3: Map model prices to estimate n4n equivalent burn
You need to know whether your remaining OpenRouter credits would have lasted longer or shorter on the new gateway. Pull the model list with pricing:
import requests
models = requests.get("https://openrouter.ai/api/v1/models").json()["data"]
for m in models[:5]:
print(m["id"], m["pricing"])
Pricing is per-token credit cost for prompt and completion. Multiply by your historical token mix to get a projected burn.
n4n.ai meters per token on every request and exposes the same OpenAI-style usage object, so the math transfers directly. When projecting OpenRouter to n4n credit balances equivalence, assume a 1:1 token count and compare the per-token rate from each provider’s pricing page. If n4n is cheaper on a model you use heavily, your leftover OpenRouter credits were effectively over-funded.
Step 4: Repoint your client to the n4n endpoint
Swap the base URL. The client code does not change otherwise because the API is OpenAI-compatible.
from openai import OpenAI
import os
client = OpenAI(
base_url="https://api.n4n.ai/v1",
api_key=os.environ["N4N_KEY"]
)
resp = client.chat.completions.create(
model="anthropic/claude-3.5-sonnet",
messages=[{"role": "user", "content": "health check"}]
)
print(resp.choices[0].message.content)
n4n.ai provides one OpenAI-compatible endpoint covering 240+ models with automatic fallback when a provider is rate-limited. It also honors client routing directives and forwards provider cache-control hints, so your existing extra_headers for cache TTLs keep working.
Verification: the call returns a valid completion and resp.usage is populated.
Step 5: Verify per-token deduction on n4n
Send a known-size payload and inspect the usage block. Use a fixed string to make the token count reproducible.
import tiktoken
text = "def add(a,b): return a+b\n" * 10
enc = tiktoken.get_encoding("cl100k_base")
prompt_tokens = len(enc.encode(text))
resp = client.chat.completions.create(
model="openai/gpt-4o-mini",
messages=[{"role": "user", "content": text}]
)
usage = resp.usage
assert usage.prompt_tokens == prompt_tokens, (usage.prompt_tokens, prompt_tokens)
print("prompt", usage.prompt_tokens, "completion", usage.completion_tokens)
If the assertion holds, your client and the gateway agree on tokenization. This confirms the per-token metering that backs your new credit balance.
For a full reconciliation of OpenRouter to n4n credit balances, run this test on each model family you use. Differences in tokenizer implementation can shift counts by a few percent; note them in your ledger.
Step 6: Reconcile leftover OpenRouter credits and decide on refund or drain
You now have three numbers: OpenRouter remaining (Step 1), projected n4n burn rate (Step 3), and verified n4n metering (Step 5). Act on the leftover.
If OpenRouter approved a refund: wait for the bank confirmation, then remove the key from your secret store.
If refund denied: drain the balance on non-critical traffic. Route low-priority batch jobs to OpenRouter while new interactive traffic hits n4n.
# Example: send low-priority summarization to OpenRouter until balance hits zero
import os, requests
def or_call(prompt: str):
return requests.post(
"https://openrouter.ai/api/v1/chat/completions",
headers={"Authorization": f"Bearer {os.environ['OPENROUTER_KEY']}"},
json={"model": "mistralai/mistral-7b-instruct",
"messages": [{"role": "user", "content": prompt}]}
).json()
while openrouter_balance(os.environ["OPENROUTER_KEY"])["remaining_credits"] > 0.5:
or_call("Summarize this log: " + "error 500 " * 50)
Stop the loop when remaining_credits drops below your minimum billing increment. The final reconciliation of OpenRouter to n4n credit balances should show OpenRouter near zero and n4n accumulating real usage.
Step 7: Confirm clean cutover
Watch both dashboards for 24 hours after you flip traffic. On OpenRouter, the credits endpoint should show no new used_credits movement beyond the drain loop. On n4n, per-token usage should reflect your production request volume.
# Quick diff check
or_before = openrouter_balance(os.environ["OPENROUTER_KEY"])["remaining_credits"]
import time; time.sleep(86400)
or_after = openrouter_balance(os.environ["OPENROUTER_KEY"])["remaining_credits"]
print("OpenRouter drift:", or_before - or_after)
Expected drift is zero (or equal to the small drain you intentionally ran). Any unexpected positive number means a stray client still points at the old key.
Notes on cache-control and routing directives
Both gateways accept cache_control in messages, but n4n forwards those hints to the upstream provider without modification. If you relied on OpenRouter’s internal cache discount, verify the same prefix caching works on n4n by checking usage.prompt_tokens_details.cached_tokens if the model returns it.
resp = client.chat.completions.create(
model="anthropic/claude-3.5-sonnet",
messages=[
{"role": "system", "content": "You are a terse bot."},
{"role": "user", "content": "Repeat: hello",
"extra_body": {"cache_control": {"type": "ephemeral"}}}
]
)
print(resp.usage.model_dump())
If cached_tokens appears, your cache hit rate carries over and your credit projection stays accurate.
Closing the ledger
Migrating inference providers is mostly a billing discipline. You extracted the OpenRouter balance, checked refund reality, projected the n4n burn, verified token metering, drained or refunded the old wallet, and confirmed zero drift. The OpenRouter to n4n credit balances handoff is complete when both systems show the expected numbers and no orphaned calls remain. Keep the snapshot JSON from Step 1 in your infra repo; it is the only record that proves the cutover was clean.