Meter firstSDK 1.4.0Audited July 29, 2026

UsageTap Meter implementation contract

This page is the canonical starting context for a coding agent. The first integration is Meter only. Preserve the application's provider, model, request shape, streaming behavior, and error behavior. Gateway, Compress, sampling, deployments, and Stripe synchronization require a separate explicit request.

One-prompt Meter setup

Implement Meter from this contract

Paste the prompt into a coding agent with repository access. It should inspect the application, implement the smallest safe integration, test it, and leave an exact setup handoff.

Open the canonical agent reference
Paste into your coding agent
Implement UsageTap Meter in this repository.

Read https://usagetap.com/llmreference first and treat it as the canonical contract. The initial goal is Meter only. Do not add UsageTap Gateway, UsageTap Compress, prompt compression, provider migration, model changes, or Stripe synchronization unless I explicitly ask.

Work autonomously:
1. Inspect the repository to find every server-side AI/provider call, the framework/runtime, existing provider SDKs, streaming behavior, tests, environment-variable conventions, and the trusted authenticated customer or tenant ID.
2. Choose the smallest safe integration. Prefer @usagetap/sdk for supported server-side JavaScript/TypeScript provider clients. Use the explicit SDK lifecycle when BLOCK or DOWNGRADE must control provider invocation or model selection; provider wrappers do not invent a safe fallback model. Use direct HTTPS for other languages or when the SDK is not a safe fit. Preserve the existing provider, model, request/response shape, and error behavior.
3. Ask one concise clarification only if a stable server-trusted customerId cannot be inferred or there are multiple materially different integration targets. Otherwise proceed.
4. Implement the complete Meter lifecycle. Attribute every call to a stable customerId and feature; begin before provider work; inspect data.allowed before invoking the provider; do not end a denied call because call_begin already finalized it; finalize an allowed call exactly once on success, error, stream completion, cancellation, or abort; report the actual model and usage returned by the provider; sanitize error metadata; and use one unique idempotency key per logical call, reused only for its retries.
5. Keep all credentials server-side. Never read, print, log, or commit secret values. Add placeholders only to the project's example environment file. UsageTap Meter needs USAGETAP_API_KEY (create a utk-… key with usage:write at https://usagetap.com/configure#api-keys). Keep the app's existing provider key such as OPENAI_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY.
6. Add or update focused tests and run the relevant test, lint, and type/build checks. If credentials are unavailable, finish and verify the code without attempting a live provider request.

When done, report:
- what you changed and which calls are now metered;
- the exact environment-variable names the user must set, without values;
- any one remaining user decision or action;
- the command or request that produces the first metered call;
- how to verify it at https://usagetap.com/embed-api?features=meter;
- the next Meter step: define customer plans, allowances, and limits at https://usagetap.com/quickstart.

Do not stop at a plan or code sample. Make the changes when it is safe to do so.

1. Required agent behavior

  1. Find every server-side provider call, including streaming, background jobs, retries, tool loops, and alternate routes.
  2. Identify a stable, server-trusted customer or tenant ID. Never trust a browser-supplied customer ID without checking it against the authenticated session.
  3. Prefer the provider wrapper for the smallest JavaScript or TypeScript change. Use the explicit SDK lifecycle when the app must enforce BLOCK or DOWNGRADE itself. Use direct HTTPS outside the supported server-side JavaScript runtime.
  4. Begin before provider work and finalize once after success, provider error, stream completion, cancellation, or abort. Report the model and usage returned by the provider.
  5. Preserve the original provider error if UsageTap finalization also fails. Make the finalization failure observable without replacing the primary error.
  6. Add focused tests and run the repository's relevant test, lint, type, and build checks.
Ask one concise question only when a stable customer ID cannot be inferred or multiple integration targets would materially change the result. Missing credentials do not block the code change: add environment variable names without values and verify without a live provider call.

2. Credentials and transport

UsageTap key

Create a Meter call key in Configure. Use a utk- key with usage:write. Store it as USAGETAP_API_KEY on the server. Meter endpoints now require the key's calls capability; admin, embed, gateway, and compression-only credentials are not interchangeable. Wrong-capability keys return CALLS_ACCESS_REQUIRED.

Provider key

Keep the provider key the app already uses, such as OPENAI_API_KEY, ANTHROPIC_API_KEY, or OPENROUTER_API_KEY. UsageTap does not replace it for a Meter integration.

  • Never expose either key to browser code, logs, commits, or agent output.
  • API authentication accepts Authorization: Bearer $USAGETAP_API_KEY or x-api-key.
  • Every API request must send Accept: application/vnd.usagetap.v1+json. Missing or incompatible Accept headers return HTTP 406.
  • JSON POST requests should send Content-Type: application/json. Responses use the versioned UsageTap media type and a standard result envelope.

3. Choose the integration path

Provider wrapper

Server-side JavaScript or TypeScript using OpenAI, OpenRouter, or Anthropic. Best for the first metered call and automatic success, error, and stream finalization.

Explicit SDK lifecycle

Use UsageTapClient.withUsage(), beginCall(), and endCall() when the app must inspect entitlements, select an approved fallback, attach custom usage, or coordinate complex streaming.

Direct HTTPS

Use for Python, Go, Java, other runtimes, unsupported providers, or when wrapping the existing client would change behavior.

The SDK requires Node.js 18.17 or newer and is server-only. Do not construct UsageTapClient in browser code. The React package is an application helper, not permission to ship a Meter key to the browser.

4. JavaScript and TypeScript SDK

Install @usagetap/sdk plus the provider package already used by the application. Import OpenAI helpers from @usagetap/sdk/openai, OpenRouter helpers from @usagetap/sdk/openrouter, and Anthropic helpers from @usagetap/sdk/anthropic.

Smallest provider-preserving integration
import OpenAI from "openai";
import { withMetering } from "@usagetap/sdk/openai";

const provider = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

export function meteredOpenAI(customerId: string) {
  return withMetering(provider, {
    apiKey: process.env.USAGETAP_API_KEY,
    customerId,
    feature: "chat.reply",
  });
}

// The OpenAI client shape is preserved.
const response = await meteredOpenAI(currentCustomer.id).responses.create({
  model: existingModel,
  input,
});

The wrapper preserves chat.completions.create() and responses.create(), generates unique idempotency keys, retries eligible UsageTap requests, extracts provider usage, and finalizes consumed or cancelled streams. It does not know the application's safe model fallback. Use the explicit path when BLOCK or DOWNGRADE must control provider selection.

Explicit SDK lifecycle with entitlement enforcement
import OpenAI from "openai";
import { UsageTapClient } from "@usagetap/sdk";

const usageTap = new UsageTapClient();
const openai = new OpenAI();

const response = await usageTap.withUsage(
  {
    customerId: currentCustomer.id,
    feature: "chat.reply",
    requested: { premium: true },
  },
  async ({ begin, setUsage }) => {
    // A blocked call is already finalized by call_begin.
    // If DOWNGRADE is configured, select a known safe fallback here.
    if (!begin.data.allowed.premium) {
      throw new Error("Premium usage is not allowed for this customer");
    }

    const result = await openai.responses.create({
      model: existingModel,
      input,
    });

    setUsage({
      modelUsed: result.model ?? existingModel,
      inputTokens: result.usage?.input_tokens ?? 0,
      responseTokens: result.usage?.output_tokens ?? 0,
      cachedInputTokens:
        result.usage?.input_tokens_details?.cached_tokens ?? 0,
    });

    return result;
  },
);

Supported SDK surface

Root client: UsageTapClient and alias UsageTap; lifecycle methods beginCall, endCall, withUsage, meter, checkUsage, createCustomer, changePlan, and incrementCustomMeter. Provider adapters include withMetering, explicit wrappers, OpenAI route streaming helpers, Express middleware, and OpenAI-compatible OpenRouter exports.

5. Direct HTTPS lifecycle

Complete success and provider-error path
const apiKey = process.env.USAGETAP_API_KEY;
const apiBase = process.env.USAGETAP_BASE_URL ?? "https://api.usagetap.com";

async function usageTapPost(path, body) {
  const response = await fetch(`${apiBase}/${path}`, {
    method: "POST",
    headers: {
      Accept: "application/vnd.usagetap.v1+json",
      "Content-Type": "application/json",
      Authorization: `Bearer ${apiKey}`,
    },
    body: JSON.stringify(body),
  });
  const envelope = await response.json();
  if (!response.ok || envelope.result?.status !== "ACCEPTED") {
    throw new Error(
      `UsageTap ${path} failed: ${envelope.result?.code ?? response.status}`,
    );
  }
  return envelope;
}

const idempotencyKey = crypto.randomUUID();
const begin = await usageTapPost("call_begin", {
  customerId: currentCustomer.id,
  feature: "chat.reply",
  requested: { premium: true },
  idempotencyKey,
});

// call_begin returns HTTP 200 for entitlement denials and has already
// finalized the denied call. Do not invoke the provider or call call_end.
if (!begin.data.allowed.premium) {
  throw new Error("Premium usage is not allowed for this customer");
}

let providerResponse;
try {
  providerResponse = await openai.responses.create({
    model: existingModel,
    input,
  });
} catch (providerError) {
  try {
    await usageTapPost("call_end", {
      callId: begin.data.callId,
      error: {
        code: "PROVIDER_ERROR",
        message: "Provider request failed",
      },
    });
  } catch {
    // Preserve the original provider error. Report finalization separately.
  }
  throw providerError;
}

await usageTapPost("call_end", {
  callId: begin.data.callId,
  modelUsed: providerResponse.model ?? existingModel,
  inputTokens: providerResponse.usage?.input_tokens ?? 0,
  responseTokens: providerResponse.usage?.output_tokens ?? 0,
  cachedInputTokens:
    providerResponse.usage?.input_tokens_details?.cached_tokens ?? 0,
});
Direct callers must send a unique explicit idempotency key for each new logical call and reuse that key only for retries of that call. The API has a deterministic fallback when the field is omitted; identical customer, feature, requested entitlement, call type, hold, and pricing inputs can therefore replay an earlier call. Do not rely on that fallback for normal traffic.

6. POST /call_begin

FieldRequiredContract
customerIdYesStable server-trusted customer or tenant identifier. Maximum 100 sanitized characters.
featureRecommendedStable application operation such as chat.reply or document.summarize.
tagsNoLow-cardinality labels. Do not include secrets or raw prompts.
requestedRecommendedEntitlements the provider call needs: standard, premium, audio, image, search, and reasoningLevel.
idempotencyKeyDirect API: yesUnique per logical call, stable across its retries. The SDK supplies one by default.
customerName / customerEmailNoOptional profile data used when the customer is first provisioned. Avoid sending data the product does not need.
batch / pricingModeNoBatch pricing metadata. pricingMode, when present, is authoritative.
Request
{
  "customerId": "tenant_123",
  "feature": "chat.reply",
  "tags": ["production"],
  "requested": {
    "standard": true,
    "premium": false,
    "audio": false,
    "image": false,
    "search": false,
    "reasoningLevel": "NONE"
  },
  "idempotencyKey": "one-key-per-logical-call"
}
Response envelope
{
  "result": {
    "status": "ACCEPTED",
    "code": "CALL_BEGIN_SUCCESS",
    "timestamp": "ISO-8601"
  },
  "correlationId": "request-correlation-id",
  "data": {
    "callId": "call-id",
    "startTime": "ISO-8601",
    "newCustomer": false,
    "canceled": false,
    "policy": "NONE | BLOCK | DOWNGRADE",
    "allowed": {
      "standard": true,
      "premium": false,
      "audio": false,
      "image": false,
      "search": false,
      "reasoningLevel": "NONE | LOW | MEDIUM | HIGH"
    },
    "entitlementHints": { "...": "advisory details" },
    "rateLimits": { "...": "optional rolling-call state" },
    "meters": { "...": "remaining, used, limit, unlimited, ratio" },
    "remainingRatios": { "...": "0 through 1, or null" },
    "subscription": { "...": "current subscription snapshot" },
    "plan": { "id": null, "name": null, "version": null },
    "balances": { "...": "optional remaining balances" },
    "idempotency": {
      "key": "one-key-per-logical-call",
      "source": "explicit | derived"
    }
  }
}
  • A blocked entitlement still returns an accepted HTTP 200 envelope with allowed set accordingly. The denied call record is already finalized; do not invoke the provider and do not call /call_end.
  • For DOWNGRADE, choose only a fallback model already approved by the application or user. Never invent a provider or model mapping.
  • Treat entitlementHints, models, and rate-limit snapshots as guidance. The allowed fields are the decision inputs.

7. POST /call_end

FieldRequiredContract
callIdYesThe exact callId returned by call_begin.
modelUsedSuccessUse the provider-returned model when available, otherwise the actual requested model.
inputTokensWhen availableTotal provider input tokens, including cache reads and cache writes.
cachedInputTokensWhen availableInput tokens read from provider cache. Do not subtract them from inputTokens.
cacheWriteInputTokensWhen availableInput tokens written to provider cache. They remain included in inputTokens.
responseTokensWhen availableProvider output or completion tokens.
reasoningTokens / searches / audioSecondsWhen availableSend actual provider usage only. Omit unavailable metrics rather than estimating them silently.
errorProvider failureA sanitized object with non-empty code and message. Do not include prompts, credentials, or raw provider bodies.
Successful provider call
{
  "callId": "call-id",
  "modelUsed": "provider-returned-model",
  "inputTokens": 1200,
  "cachedInputTokens": 400,
  "cacheWriteInputTokens": 0,
  "responseTokens": 180,
  "reasoningTokens": 0,
  "searches": 0,
  "audioSeconds": 0
}
Provider failure
{
  "callId": "call-id",
  "error": {
    "code": "PROVIDER_ERROR",
    "message": "Provider request failed"
  }
}
Accepted response
{
  "result": {
    "status": "ACCEPTED",
    "code": "CALL_END_SUCCESS",
    "timestamp": "ISO-8601"
  },
  "correlationId": "request-correlation-id",
  "data": {
    "callId": "call-id",
    "status": "COMPLETED | FAILED",
    "error": { "code": "optional", "message": "optional" },
    "costUSD": 0.00123,
    "standardCostUSD": 0.00123,
    "pricingMultiplier": 1,
    "usage": {
      "inputTokens": 1200,
      "cachedInputTokens": 400,
      "cacheWriteInputTokens": 0,
      "billableInputTokens": 800,
      "responseTokens": 180,
      "reasoningTokens": 0
    },
    "metered": { "...": "usage applied to configured meters" },
    "spendVelocity": { "...": "optional hour and day snapshots" },
    "meters": { "...": "updated meter snapshot" },
    "balances": { "...": "updated balances" }
  }
}

/call_end is idempotent. A repeated finalization does not decrement meters twice. The SDK error object is a first-class API contract: an accepted provider-error finalization returns data.status = "FAILED" while preserving the normalized error metadata.

8. Streaming, retries, and failure policy

  • Finalize after the stream is consumed, not when headers arrive. Include final usage events from the provider when its streaming option must be enabled explicitly.
  • Finalize on consumer cancellation and abort. Do not discard a wrapped stream without consuming or cancelling it.
  • Retry a UsageTap request only with the same idempotency key and call ID for the same logical call. Never generate a new key inside a retry loop.
  • The SDK defaults to three total attempts with bounded backoff and jitter for retryable UsageTap failures. Provider retries remain controlled by the existing provider integration.
  • Decide and document fail-open versus fail-closed behavior for a UsageTap outage. Access enforcement generally fails closed; observability-only metering may fail open if the product owner accepts missing usage.

9. Customer and Meter setup after code

  1. Set USAGETAP_API_KEY and the existing provider key in the application's server runtime.
  2. Run the exact local command or request identified by the coding agent to produce one provider call.
  3. Confirm the connection and customer attribution on Code Integration.
  4. Configure customer plans, call and token allowances, limits, replenishment, and optional custom meters in Meter setup.
  5. Exercise one allowed call and one limit path before production.

POST /customers and usageTap.createCustomer() are idempotent and can provision a customer before their first model call. GET /customers/{customerId}/usage and usageTap.checkUsage() return the current entitlement and meter snapshot. Plan changes require an admin key and should not be embedded in the initial application integration.

10. Explicitly outside first onboarding

Do not add UsageTap Gateway, prompt compression, Compress keys, sampling or payload capture, deployment resolution, provider migration, model changes, Stripe synchronization, browser embed sessions, or admin automation unless the user asks. The SDK exposes several of these advanced capabilities, but their presence is not permission to enable them.

11. Agent completion contract

The task is complete only when:

  • the intended server-side provider calls are actually metered;
  • customer attribution is stable and authenticated;
  • success, failure, retry, streaming, cancellation, and limit behavior are handled;
  • keys remain server-only and no values were logged or committed;
  • focused tests and repository checks pass;
  • the handoff names every required environment variable, the first-call command, the verification page, and the remaining Meter configuration action.

Browser authentication

12. Secure Gateway sessions for browser chat

Keep the existing UsageTap key on the customer's server. Their authenticated backend exchanges it for a customer-bound, five-minute client token that the chat panel can use directly. A Gateway key gets gateway:chat; a Compression key gets compression:run; a combined key gets both. The app's normal login session is the renewal authority, so there is no separate refresh token to store. If the parent key can use Compression, the temporary session inherits that capability too.

  1. 1. Authenticate

    The customer backend verifies its user and resolves stable UsageTap customer and user IDs.

  2. 2. Mint

    The backend creates a five-minute session with the parent key's effective Gateway and Compression capabilities.

  3. 3. Call and renew

    The browser keeps the token in memory, calls Gateway, and asks its backend for another token before expiry.

1. Customer backend: mint and renew

Adapt the authentication and identity lookup to the application. The browser must never choose either identifier.

app/api/usagetap/gateway-session/route.ts
import { NextResponse } from "next/server";
import { requireUser } from "@/lib/auth";
import { usageTapIdentityForUser } from "@/lib/usagetapCustomers";

// GET /api/usagetap/gateway-session
export async function GET() {
  const user = await requireUser();

  // Resolve UsageTap-safe, stable IDs from the authenticated user.
  // Never accept either ID from the browser.
  const identity = await usageTapIdentityForUser(user.id);

  const response = await fetch(
    "https://api.usagetap.com/v1/client/sessions",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.USAGETAP_API_KEY}`,
        Accept: "application/vnd.usagetap.v1+json",
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        customer_id: identity.customerId,
        customer_user_id: identity.customerUserId,
      }),
      cache: "no-store",
    },
  );

  const envelope = await response.json();
  if (!response.ok) {
    return NextResponse.json(
      { error: "Unable to create a client session" },
      { status: response.status },
    );
  }

  return NextResponse.json(
    {
      token: envelope.data.token,
      expiresAt: envelope.data.expires_at,
    },
    { headers: { "Cache-Control": "no-store" } },
  );
}

2. Browser: hold, renew, and retry once

The shared renewal promise prevents several simultaneous chat calls from minting several sessions.

lib/usagetapGateway.ts
type ClientSession = {
  token: string;
  expiresAt: string;
};

let session: ClientSession | undefined;
let renewal: Promise<ClientSession> | undefined;

async function gatewayToken(forceRenew = false) {
  const validForMs = session
    ? Date.parse(session.expiresAt) - Date.now()
    : 0;

  if (!forceRenew && session && validForMs > 60_000) {
    return session.token;
  }

  renewal ??= fetch("/api/usagetap/gateway-session", {
    credentials: "same-origin",
    cache: "no-store",
  }).then(async (response) => {
    if (!response.ok) throw new Error("Unable to renew Gateway session");
    return response.json() as Promise<ClientSession>;
  });

  try {
    session = await renewal;
    return session.token;
  } finally {
    renewal = undefined;
  }
}

async function gatewayRequest(
  path: "/chat/completions" | "/responses",
  body: unknown,
  retry = true,
) {
  const response = await fetch(`https://gateway.usagetap.com/v1${path}`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${await gatewayToken()}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(body),
  });

  if (response.status === 401 && retry) {
    session = undefined;
    await gatewayToken(true);
    return gatewayRequest(path, body, false);
  }
  if (!response.ok) throw new Error(`Gateway request failed: ${response.status}`);
  return response;
}

// Keep the session only in memory. This works for JSON or streamed responses.
const response = await gatewayRequest("/chat/completions", {
  model: "usagetap/standard",
  messages,
  stream: true,
  // Optional when the server key has compression:invoke.
  // Omit this to let centrally configured Runtime Compression decide.
  compress: true,
});

Security boundary

Keep USAGETAP_API_KEY server-only. Do not put either credential in localStorage, sessionStorage, a query string, logs, or a public environment variable. Session claims supply customer attribution and restrict access to chat completions and responses; they cannot list models or run batches. Hosted and configured Runtime Compression work only when the minting key has compression:invoke.

Coding-agent handoff

Implement the secure browser path

This prompt asks a coding agent to find the app's trusted login and customer mapping, add the mint endpoint and in-memory renewal, preserve streaming, and test the security boundary.

Paste into your coding agent
Implement secure UsageTap Gateway authentication for the browser chat panel in this repository.

Read https://usagetap.com/llmreference#browser-gateway-sessions first and treat it as the canonical contract. The goal is only to let authenticated application users call UsageTap Gateway without exposing the customer's long-lived key. Do not create a new API key type, add origin allowlists, or change providers or models unless the repository or user explicitly requires it.

Work autonomously:
1. Inspect the application's authentication, tenant or account model, server framework, chat client, environment-variable conventions, and tests.
2. Find the stable UsageTap customer ID and optional stable end-user ID that belong to the authenticated application user. Resolve and validate both on the server. Never accept a customer or user ID from browser input.
3. Add a same-origin authenticated endpoint such as GET /api/usagetap/gateway-session. It must use the existing server-only USAGETAP_API_KEY with gateway:invoke to POST https://api.usagetap.com/v1/client/sessions. Send Accept: application/vnd.usagetap.v1+json, Content-Type: application/json, customer_id, and optional customer_user_id. UsageTap derives the temporary capabilities from the parent key: gateway:invoke becomes gateway:chat and compression:invoke becomes compression:run. Never let browser input choose or widen scopes.
4. Return only data.token and data.expires_at to the browser with Cache-Control: no-store. Never expose the long-lived key through NEXT_PUBLIC_, VITE_, browser bundles, browser storage, query strings, logs, errors, or committed values.
5. Add a small browser token manager. Keep the five-minute bearer token in memory, share one in-flight renewal between concurrent requests, renew through the authenticated same-origin endpoint about 60 seconds before expiry, and retry a Gateway request at most once after a 401. There is no separate refresh token; the application's existing login session is the renewal authority.
6. Send the short-lived token in Authorization: Bearer to https://gateway.usagetap.com/v1/chat/completions or /v1/responses. Preserve the existing request, streaming, model, Compression, response, cancellation, and error behavior. A Compression-capable session may use the normal compress option, while centrally configured Runtime Compression requires no browser flag. Do not send customer identity in the Gateway request because the signed session identity is authoritative.
7. Add focused tests for unauthenticated minting, trusted identity lookup, upstream mint failure, no-store responses, concurrent renewal, renewal before expiry, and one-time 401 retry. Run the relevant tests, lint, typecheck, and build checks.

When done, report the files changed, the server-only environment-variable name without its value, the trusted customer and user identity sources, the chat call sites using the short-lived token, the checks run, and how to verify that browser-supplied identity cannot cross customer boundaries.

Do not stop at a plan or code sample. Make the changes when it is safe to do so.

See the canonical browser-session contract and the API reference.

Contract audit: SDK package 1.4.0 and the deployed API handler were compared on July 29, 2026. This revision aligns key capabilities, media negotiation, idempotency, cache-write token reporting, rate-limit and spend snapshots, and provider-error finalization. The browser Gateway session contract was added August 20, 2026.