← All posts
Guides

Ship Your First AI Feature: Signup to Production in an Afternoon

A step-by-step path from a funded nRouter account to a production LLM call with a spend ceiling on it — create a scoped key, point your existing OpenAI client at one base URL, cap the key, and verify the cost header came back.

nRouter team · 11 min read
Ship Your First AI Feature: Signup to Production in an Afternoon

The short answer: the work between a demo and a production AI feature is not the model. It is a provider account, an SDK, a spend ceiling and a log. Through a gateway all four collapse into: fund an organization, create a scoped virtual key, change base_url to https://api.nrouter.ai/v1, attach a budget to the key, and read x-nr-request-cost off the first response.

The short answer

The demo always works. It is the four weeks after the demo that quietly eat a quarter — a provider account waiting on approval, a second SDK to learn when someone asks for a cheaper model, a budget cap that finance wants before the rollout, and request logging that nobody built until the first surprise invoice arrived.

None of that is your feature. It is the tax every team pays to put any model in front of a real user, and it is paid again per provider. This guide is the afternoon version: five steps, each with a command or an exact dashboard field, ending with a call that is capped, attributed, and logged. The end state is not "it returned a completion" — a curl one-liner does that. The end state is a call you can safely leave running when you close the laptop.

When you need this

You have a working prototype and no idea what it will cost in production. The prototype ran on a personal key with no ceiling. Before it can serve real users somebody has to answer "what stops this from spending $9,000 in a weekend", and right now the honest answer is "nothing".

Product asked to try a second model and you quoted two weeks. That estimate is not about the model. It is about a second account, a second SDK, a second set of credentials in your secret manager, and a second billing dashboard that has to be reconciled against the first at month end.

Somebody asked which team the spend belongs to and the answer was a shrug. One shared key means one undifferentiated line item. Attribution is not something you can backfill; it has to exist at the moment the request is made, which means it has to exist before you launch. The mechanics of doing that properly are in Attribute LLM spend by team, customer, and feature.

If any of those is true, the constraint on your ship date is governance, not capability — and governance is a configuration problem, not a build.

What you need first

Four things, and three of them take under five minutes.

  • A funded organization. Signup is card-required and takes a real charge; the minimum credit purchase is $5, and a first qualifying purchase carries a $10 bonus — load $5, get $15. There is no BYOK step here: you never paste a provider key, because nRouter holds the provider credentials. The reasoning is in Why we don't do BYOK. Start at /signup; the fee that rides on top is on Pricing.
  • A terminal with curl, or Python/Node with the OpenAI SDK already installed. You are not adding a dependency. That is the point of step 3.
  • NROUTER_API_KEY exported, which step 2 produces. Every code block below is runnable as written against https://api.nrouter.ai/v1.
  • Owner or admin role if you want to attach budgets at organization or team scope. A key-level budget can be set by whoever creates the key. Roles are laid out in Team Management.

You do not need a provider account, an approval queue, a second SDK, or a credit-card conversation with a vendor's sales team. Skipping that is worth more than it sounds: a direct provider account is a second rate-limit budget to plan around (Anthropic publishes its per-organisation RPM and TPM ceilings at docs.anthropic.com) and a second rate card to reconcile at month end (OpenAI).

Step 1 — Fund the organization and confirm the balance

Credits are the floor under everything else. Before any request leaves the gateway, the estimated cost of that call is reserved against the organization's balance; if the balance cannot cover the reservation, the call returns 402 and no provider is contacted. That is the mechanism that makes a spend ceiling a ceiling rather than a suggestion, and it is described in Reserve-and-settle.

Load the first amount on the Billing page (/[organization]/billing). The $5 floor is a floor, not a menu — anything above it is a number you type. Two things worth knowing before you pick it:

  • The platform fee is charged on top of the amount you choose, so a $5 load puts $5 on the balance and bills the fee separately. It is never skimmed off the credits.
  • The first qualifying purchase adds the $10 bonus after the payment clears, not before. A balance of $15 from a $5 purchase is the expected end state, and every movement lands in the ledger described in How to read your LLM credit ledger.

If a runaway job is a realistic risk in your environment, decide now whether to turn on auto-topup — it removes the "balance hit zero at 2am" failure mode and introduces a different one, which is exactly what Auto Top-Up Without Surprise Bills: Threshold, Amount, Cap is about. You can skip it for an afternoon build and revisit it before real traffic.

Step 2 — Create a scoped virtual key, not a shared one

Go to the Keys page (/[organization]/keys) and click Generate Key. The temptation at this point is to make one key called api-key and move on. Resist it for ninety seconds, because the fields on this form are the entire difference between a leak you revoke and a leak you have an incident review about.

FieldWhat to put in itWhy it matters later
Nameprod-<service>, e.g. prod-ticket-summarizerThe dashboard sorts alphabetically, so the prefix groups environments
TeamThe team that owns this workloadSpend and limits attribute here; a key belongs to exactly one team
BudgetA per-key cap in USDThe ceiling that survives you forgetting about the key
Allowed modelsLeave empty, or list gpt-5.4-miniAn empty list means everything the team may use
Allowed endpointsRestrict to what the service callsA summarizer needs /chat/completions and nothing else
IP allowlistCIDR blocks, or blankDenied requests are rejected before the key is consumed
ExpirationNever, 30 days, 90 days, or a datetime90 days is the sane default for service-to-service

The full value is shown exactly once, at creation. After that the dashboard shows the key name and last four characters and nothing else — there is no reveal button, because the full value is not retrievable. Copy it now and put it in your secret manager, not in a Slack message to yourself.

export NROUTER_API_KEY="sk-nrouter-..."   # shown once, at creation
curl -sS https://api.nrouter.ai/v1/models \
  -H "Authorization: Bearer $NROUTER_API_KEY" | head -c 400

A model list means you are authenticated. A 401 means the key is wrong, revoked, or expired — check Authentication. The reason one key per (environment, service) pair beats one key for everything is the whole argument of Virtual keys vs master key, and it is worth reading before you get to five services.

Step 3 — Point the SDK you already have at one base URL

This is the step that people expect to be hard and is not. The API implements OpenAI's Chat Completions contract, and OpenAI's own maintained clients — the Python SDK and the Node SDK — accept a base_url override, so the integration is two arguments on a client you already import. There is no nRouter SDK to install and no second client object to keep in step with your first one.

import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["NROUTER_API_KEY"],
    base_url="https://api.nrouter.ai/v1",
)

resp = client.chat.completions.with_raw_response.create(
    model="gpt-5.4-mini",
    messages=[{"role": "user", "content": "Summarize this ticket in one line."}],
)

print(resp.headers.get("x-nr-request-id"))     # always present — store it
print(resp.headers.get("x-nr-request-cost"))   # may be ABSENT — never default to 0
print(resp.parse().choices[0].message.content)

The same two arguments in Node:

import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.NROUTER_API_KEY,
  baseURL: "https://api.nrouter.ai/v1",
});

Use with_raw_response (or the equivalent in your language) from the first call rather than retrofitting it later. The response headers carry the request id and the settled cost, and capturing the request id on every call — success and failure — is what makes a support ticket a lookup instead of a timestamp hunt. Language-by-language snippets live under SDKs; if you are inside LangChain, LlamaIndex, CrewAI or the Vercel AI SDK, the same base-URL swap is documented per framework at Frameworks.

Step 4 — Attach a budget before you launch, not after

A key with no cap is a key that can spend the whole balance. Create the ceiling on the Budgets page (/[organization]/budgets) → Create Budget. Three fields decide the behaviour:

FieldWhat it doesSensible first value
Budget NameThe label that appears inside the error messageTicket summarizer — daily
Max SpendThe hard cap in USDTen times your expected daily spend
DurationDaily, Weekly, Monthly, or Total (never resets)Daily

Then pick an enforcement mode. Block rejects at the cap, Warn alerts and keeps serving, Throttle alerts and flags the budget for rate reduction. A production ceiling is Block, with a warn threshold underneath it as the early signal — using Warn alone and calling it a cap is the single most common mistake here.

The scope you attach it to decides the status code you get back, and this catches people:

  • Organization, team and user budgets return 402 with code budget_exceeded.
  • Per-key budgets return 429 with code key_budget_exceeded.

That is not a typo, and your retry logic has to branch on the code field rather than the status. Both are stock HTTP — 402 is the payment-required status reserved by RFC 9110 and 429 Too Many Requests is RFC 6585 — so the status alone cannot tell two very different situations apart: a 429 from a rate limit clears on its own in seconds, a 429 from a key budget will not clear until the window resets. The client-side playbook is Handling 429 and 402 errors.

Pair the dollar cap with an RPM and TPM limit on the same key. A monthly budget is a total and structurally cannot stop a retry loop from consuming it in an afternoon; velocity is a separate control, and the two together are covered in Budgets vs rate limits.

Step 5 — Change the model string, not the integration

Everything up to here was setup you do once. This step is what you bought.

CHEAP = "gpt-5.4-mini"
STRONG = "gpt-5.5"

model = STRONG if ticket.get("priority") == "p1" else CHEAP
resp = client.chat.completions.create(model=model, messages=msgs)

Adding a second model is a string. Adding a provider you have never used is also a string, because the account, the credential and the request-shape translation are not yours. There is no second SDK, no second key in the secret manager, no second invoice to reconcile, and no second cost table to maintain. Browse what is currently servable on Models.

Once more than one model is live in the same code path, the interesting question stops being "does it work" and becomes "which one should get this request" — which is the subject of Cost-vs-Quality LLM Routing: Which Tasks Can Go Cheap, and of Provider fallback chains when the answer needs to survive an upstream outage.

Verifying it worked

Four observable signals, in the order you should check them. Configuration you have never watched fire is a hypothesis.

  1. The response carried a request id and a cost. Dump the headers:
curl -sS -D - -o /dev/null https://api.nrouter.ai/v1/chat/completions \
  -H "Authorization: Bearer $NROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-5.4-mini","messages":[{"role":"user","content":"ping"}]}' \
  | grep -i '^x-nr-'

You should see x-nr-request-id and, when the call was priceable, x-nr-request-cost in USD. x-nr-request-cost is absent when a call cannot be priced — absent is unknown, and code that defaults it to 0.0 will under-report your own spend while the ledger stays right.

  1. The row appeared in the logs. Open /[organization]/logs and paste the request id into the search box. The row carries the model, token counts, latency, status and settled cost. What a log row should and should not contain is What an LLM request log should contain.

  2. The ledger moved. The credit balance decreased by the settled cost, and a transaction exists for it. Spend charts and the ledger answer different questions; the ledger is the authoritative one.

  3. The cap actually bites. Attach a $0.01 Total budget to a throwaway key, send two calls, and confirm the second returns 429 with key_budget_exceeded and a message naming the budget. Then delete the throwaway key. You now have a ceiling you have personally watched reject a request, which is a materially different claim from a ceiling you configured.

What goes wrong

You shipped with one key for everything. It works, right up until it leaks or one workload eats another's headroom. A shared key also destroys per-service attribution, and attribution cannot be reconstructed after the fact. One key per (environment, service) pair, always.

You put the budget at organization scope and expected it to protect a team. An org budget is the backstop on the whole account; it will happily let one service consume the entire thing. If the concern is "the batch job must not spend the chat feature's money", the budget belongs on the key or the team. Scope resolution is in Org, team, member.

You defaulted the missing cost header to zero. This one is silent, which is what makes it expensive. An absent x-nr-request-cost means the call could not be priced, not that it was free. Treat absent as unknown, and reconcile against the ledger rather than summing headers.

You retried a budget block with exponential backoff. A 429 carrying key_budget_exceeded is a policy outcome, not a transient failure. Generic retry logic will spin against it until the window resets, adding latency and load and achieving nothing.

You hardcoded the model string in fifteen call sites. The whole benefit of a single integration is that swapping a model is a one-line change. That is only true if the model name lives in configuration. Put it in one place on day one; retrofitting it is a refactor nobody schedules.

Try it

Everything in this guide — per-key budgets, RPM and TPM limits, guardrails, request logs, A/B tests, prompt management and evals — is available on every plan. Plans vary the platform fee (4% on Pay as you go, 0% on Pro at $50/mo or $500/yr) and the default rate limits, never the feature set. Pro pays for itself once 4% of monthly spend clears $50, which is around $1,250/mo of provider spend; the arithmetic is on Pricing.

Load your first $5 and get a $10 bonus — $15 in API credits — then work the five steps above. Start at /signup, or send a first request from the browser in the Playground before you write any code at all.

Stuck on a step? The canonical reference for the same task is Quick Start, and people answer questions in the nRouter community.

See also

Sources

Verified 2026-06-24; the external references were re-checked on 2026-08-23. Every number, field name, dashboard path and status code above comes from nRouter's own documentation or pricing page. If something has drifted, email hello@nrouter.ai and we will correct it.

Share
Written by nRouter teamEngineering, product, and company posts from the nRouter team — code-first, cost-honest, no vendor-marketing fluff.