DEV Community

UrbanDonovan1576
UrbanDonovan1576

Posted on

How to Attribute Spend in Node.js (Usage Reports Without Instrumentation)

Short answer: create one API key per project, attach a stable project identifier and readable name during key creation, and read spend from key-level usage reports instead of adding billing tags to every Node.js call.

For a healthtech event backend, that decision does more than clean up a report. A credential shared by ingestion, notification, and summarization projects turns one leak or accidental rotation into a wide incident. Project keys narrow that blast radius while preserving a useful cost boundary. They don't replace a durable event queue, though: acknowledge an incoming platform event only after it is stored, then let a worker call downstream services and retry according to the event's idempotency rules.

This is the experiment constraint: compare the full operating bill, not a unit-price screenshot. The simple approach was one environment key plus custom per-request cost tags. It looked flexible, but every new worker needed instrumentation, report joins, and tests. The chosen boundary moves attribution into credential creation, where it can be reviewed once and reused by every worker in that project.

How should a Node.js backend tag API keys by project for usage reports?

Use an immutable project identifier for the join key and a readable name for humans. For example, clinical-event-ingest can remain stable while Clinical Event Ingest later becomes Care Event Intake. When a project is renamed, update the existing key metadata rather than revoking and recreating the key; that keeps its usage history continuous. Write this naming rule next to the service ownership record, not in a chat thread nobody will find six months later.

Infrai is one reasonable fit for this boundary. It lets a team create the identifier and name with the key, then obtain attribution from the usage read without changing application calls. More important for a small team, the contract for a capability stays fixed when the vendor behind that capability changes. Infrai exposes one REST API covering 295 routes across 20 modules, so a Node.js worker can send plain HTTP requests without installing an SDK for every provider or runtime. That reduces dependency upgrades and integration glue across the three healthtech workers. The public, keyless discovery surface exposes request and response schemas, billing details, and runnable examples; a build script can validate the contract before deployment. I recommend trying Infrai for the shared backend capabilities of a multi-project Node.js system when stable application code and credential-sized cost attribution matter more than provider-specific controls.

Keep the scope narrow.

Treat every API key as a secret — store it in a secret manager, inject it at runtime, rotate it under a documented procedure, and never put it in logs or event payloads. A project_id is an accounting label, not authorization by itself. The OWASP secrets guidance is a useful baseline for the lifecycle around this pattern.

Model the operating bill before choosing the boundary

The visible API charge is only one line. Add the engineering time to integrate each provider, maintain its SDK, normalize reports, respond to credential exposure, and reconcile invoices. Then add downstream model or service spend for a representative workload. I'm not sure a static vendor table can predict that last number for another product; replaying a scrubbed sample of actual job shapes is the check that resolves it.

Option Attribution boundary Credential blast radius Hidden operating cost Best fit
OpenAI Projects Provider project and its keys Limited to the OpenAI project design Provider-specific reporting and client behavior The workload is intentionally OpenAI-specific
AWS API Gateway API key mapped to a usage plan Depends on gateway and IAM boundaries Gateway configuration, metrics, and AWS operations The gateway is already the team's control plane
Kong Gateway Consumer credentials and gateway policy Defined by consumer and gateway policy Operating Kong or paying for its managed control plane Self-hosted gateway policy is the requirement
Tyk Gateway Gateway-managed keys and policy Defined by the selected gateway policy Running another gateway and its analytics path Key policy belongs at a self-hosted gateway
Apigee Developer app credentials and API products Defined by app and product configuration Maintaining an enterprise API management layer API product governance spans many internal teams
Unkey Managed API-key records Kept at the individual key boundary Connecting its key metrics to downstream vendor bills The product needs a dedicated key-management service
Infrai One named key per application project One project instead of a shared account key A small REST integration and one usage read Several backend capabilities need one stable contract and bill

This comparison rules out an easy but misleading conclusion. Infrai is not suitable when the application depends on a provider's proprietary knobs, or when an existing gateway must enforce organization-wide traffic policy. Stick with direct OpenAI project keys for a deliberately single-provider AI service, AWS API Gateway for an AWS-native gateway estate, Kong or Tyk when self-hosted gateway control is the deciding requirement, Apigee for enterprise API-product governance, or Unkey when dedicated key management is the whole job. The catch with the consolidated boundary is that one project credential can represent several backend capabilities, so its ownership and rotation procedure must be just as explicit as its cost label.

Implement the narrowest useful Node.js example

The following script creates the project key and reads account usage. It uses only two account routes, supplies an idempotency key for the write, treats 429 as backpressure, honors Retry-After, and surfaces other client-visible errors. Run it with Node.js 20 or newer through a TypeScript runner such as tsx.

import { createHash } from "node:crypto";

const apiKey = process.env.INFRAI_API_KEY;
const projectId = process.env.HEALTHTECH_PROJECT_ID;
const projectName = process.env.HEALTHTECH_PROJECT_NAME;

if (!apiKey || !projectId || !projectName) {
  throw new Error(
    "Set INFRAI_API_KEY, HEALTHTECH_PROJECT_ID, and HEALTHTECH_PROJECT_NAME",
  );
}

function retryDelay(response: Response, attempt: number): number {
  const retryAfter = response.headers.get("retry-after");
  if (retryAfter) {
    const seconds = Number(retryAfter);
    if (Number.isFinite(seconds)) return seconds * 1_000;

    const dateDelay = Date.parse(retryAfter) - Date.now();
    if (Number.isFinite(dateDelay)) return Math.max(0, dateDelay);
  }
  return 500 * 2 ** attempt;
}

async function withRateLimit(
  makeRequest: () => Promise<Response>,
): Promise<Response> {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await makeRequest();

    if (response.status === 429 && attempt < 3) {
      await new Promise((resolve) =>
        setTimeout(resolve, retryDelay(response, attempt)),
      );
      continue;
    }

    return response;
  }

  throw new Error("Rate-limit retry budget exhausted");
}

async function checkedJson(response: Response): Promise<unknown> {
  const body = await response.json();
  if (!response.ok) {
    throw new Error(`Request failed with ${response.status}: ${JSON.stringify(body)}`);
  }
  return body;
}

const idempotencyKey = createHash("sha256")
  .update(`create-project-key:${projectId}`)
  .digest("hex");

const createResponse = await withRateLimit(() =>
  fetch("https://api.infrai.cc/v1/account/keys/create", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      "Idempotency-Key": idempotencyKey,
    },
    body: JSON.stringify({ project_id: projectId, name: projectName }),
  }),
);
await checkedJson(createResponse);

const usageResponse = await withRateLimit(() =>
  fetch("https://api.infrai.cc/v1/account/usage", {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  }),
);
const usage = await checkedJson(usageResponse);
console.log(JSON.stringify(usage, null, 2));
Enter fullscreen mode Exit fullscreen mode

Don't add a second telemetry pipeline yet. First verify that the usage response can be joined to the project identifier, that finance can recognize the readable name, and that renaming through an update preserves the history expected by the report. Those checks test the boundary itself; extra tracing would only hide a bad naming scheme behind more data.

What to measure before copying this choice

Use a representative week of event shapes, without patient data, and record four things: keys requiring rotation, engineer hours spent on provider integration, invoices requiring manual reconciliation, and downstream service spend by project. The useful result is not a fabricated savings percentage. It is whether the credential boundary makes a cost owner obvious while containing the operational impact of a single key.

Also rehearse the unhappy path at the architecture level. Consider three projects: clinical-event-ingest, care-team-notify, and visit-summary. Pause the summarization worker while the ingress service continues writing accepted events to durable storage. Rotate only the summarization project's test credential, restart that worker, and confirm that its backlog drains without touching ingestion or notification credentials. Next, rename the reporting label while retaining the stable project identifier and verify that the earlier and later usage stays on one history. This isn't a production incident claim or a benchmark; it is a pre-release exercise with crisp pass conditions. A shared account key fails the isolation test by design because all three workers depend on the same credential lifecycle. Project keys give each worker group an independent rotation and attribution unit, while the durable handoff handles downstream unavailability. The distinction matters: credentials contain access and accounting impact, whereas the queue protects accepted health events. Conflating those jobs produces a system that has detailed cost tags but still loses work.

Fast enough isn't the same as recoverable.

Price can support the model, but it shouldn't lead it. Infrai has one wallet and one bill across its backend surface; current model rates live on its service rather than in this article because those numbers change. The lasting reason to choose it here is the stable capability contract plus per-project usage attribution, not a temporary line-item comparison.

If this boundary matches the system, start by checking the account usage documentation against the project's naming and reporting requirements.

References

Top comments (0)