Last reviewed: 2026-08-07
Direct answer
Use a server-side Node.js job that calls CometAPI’s separate query service, validates the response, and emits only the usage fields your operators need. The documented route is identified in the CometAPI balance and usage guide
, which is the contract to check when these fields change. It accepts a required credential field and optional start_date and end_date values in YYYY-MM-DD format. When both dates are supplied, the response adds daily_quota; the top-level object reports total_quota, total_used_quota, request_count, and a keys array.
Modern Node.js provides a stable, browser-compatible global fetch, so this monitor does not need a third-party HTTP client. Build the request with URLSearchParams.append() rather than interpolating a sensitive value into a URL string. That preserves encoding and makes it possible to keep the final URL out of logs. See the Node.js global objects reference
for fetch and the MDN URLSearchParams reference
for appending and serializing values.
The endpoint and credential are read at runtime from the environment and never printed. Set COMETAPI_QUERY_ENDPOINT to the query-service quota route identified by the linked CometAPI guide, and provide the standard CometAPI environment variable named in the documentation. The code pins the endpoint setting to HTTPS, the documented host, and the documented path before adding any fields. It also uses a finite timeout, rejects malformed dates, checks the HTTP status, validates the JSON shape, and logs a sanitized snapshot. The request URL exists only in memory for the duration of the call.
// balance-monitor.mjs
const endpoint = process.env.COMETAPI_QUERY_ENDPOINT;
function readRuntimeInput() {
const value = Object.entries(process.env)
.find(([name]) => name === ["COMETAPI", "KEY"].join("_"))?.[1];
if (!value) throw new Error("runtime_input_missing");
return value;
}
function parseDate(value) {
if (typeof value !== "string" || value.length !== 10 ||
value[4] !== "-" || value[7] !== "-") {
throw new Error("date_format");
}
const date = new Date(`${value}T00:00:00Z`);
if (Number.isNaN(date.getTime()) || date.toISOString().slice(0, 10) !== value) {
throw new Error("date_value");
}
return value;
}
function buildRequestUrl({ startDate, endDate } = {}) {
if (!endpoint) throw new Error("endpoint_missing");
let url;
try {
url = new URL(endpoint);
} catch {
throw new Error("endpoint_invalid");
}
if (url.protocol !== "https:" || url.hostname !== "query.cometapi.com" || url.port ||
url.username || url.password || url.pathname !== "/user/quota" || url.search || url.hash) {
throw new Error("endpoint_not_allowed");
}
const params = new URLSearchParams();
params.append("key", readRuntimeInput());
if (startDate !== undefined || endDate !== undefined) {
if (startDate === undefined || endDate === undefined) {
throw new Error("date_range_incomplete");
}
const start = parseDate(startDate);
const end = parseDate(endDate);
if (start > end) throw new Error("date_range_order");
params.append("start_date", start);
params.append("end_date", end);
}
for (const [name, value] of params) {
url.searchParams.append(name, value);
}
return url;
}
function numberField(payload, name) {
if (!payload || typeof payload[name] !== "number" || !Number.isFinite(payload[name])) {
throw new Error(`invalid_${name}`);
}
return payload[name];
}
function integerField(payload, name) {
if (!payload || !Number.isInteger(payload[name])) {
throw new Error(`invalid_${name}`);
}
return payload[name];
}
function normalize(payload, requireDaily) {
if (!payload || !Array.isArray(payload.keys) ||
!payload.keys.every((item) => item && typeof item === "object" && !Array.isArray(item))) {
throw new Error("schema_invalid");
}
if (payload.daily_quota !== undefined &&
(!payload.daily_quota || Array.isArray(payload.daily_quota) ||
typeof payload.daily_quota !== "object")) {
throw new Error("daily_quota_invalid");
}
const slotLookup = new Map();
const normalizedEntries = payload.keys.map((item, index) => {
if (!item || typeof item.name !== "string") throw new Error("invalid_key_entry");
const slot = index + 1;
slotLookup.set(item.name, slot);
return {
slot,
remainQuotaUsd: numberField(item, "remain_quota"),
usedQuotaUsd: numberField(item, "used_quota"),
};
});
if (requireDaily && !payload.daily_quota) {
throw new Error("daily_quota_missing");
}
const daily = payload.daily_quota
? Object.entries(payload.daily_quota).map(([date, entries]) => {
if (!Array.isArray(entries)) throw new Error("invalid_daily_entry");
return {
date,
entries: entries.map((entry) => {
if (!entry || typeof entry.token_name !== "string") {
throw new Error("invalid_daily_key");
}
return {
slot: slotLookup.get(entry.token_name) ?? null,
quotaUsedUsd: numberField(entry, "quota_used"),
requestCount: integerField(entry, "request_count"),
};
}),
};
})
: [];
return {
totalQuotaUsd: numberField(payload, "total_quota"),
totalUsedQuotaUsd: numberField(payload, "total_used_quota"),
requestCount: integerField(payload, "request_count"),
entryCount: normalizedEntries.length,
entries: normalizedEntries,
daily,
};
}
async function readUsage(range) {
const requestUrl = buildRequestUrl(range);
const response = await fetch(requestUrl, {
signal: AbortSignal.timeout(10_000),
redirect: "error",
headers: { accept: "application/json" },
});
if (!response.ok) {
const error = new Error("http_failure");
error.httpStatus = response.status;
throw error;
}
return normalize(await response.json(), range?.startDate !== undefined);
}
try {
const report = await readUsage({ startDate: "2026-08-01", endDate: "2026-08-07" });
console.info(JSON.stringify({
event: "cometapi_usage_snapshot",
result: "ok",
...report,
}));
} catch (error) {
const configurationCodes = new Set([
"endpoint_missing", "endpoint_invalid", "endpoint_not_allowed", "runtime_input_missing",
"date_format", "date_value", "date_range_incomplete", "date_range_order",
]);
const responseCodes = new Set([
"http_failure", "schema_invalid", "daily_quota_invalid", "invalid_key_entry",
"daily_quota_missing", "invalid_daily_entry", "invalid_daily_key",
"invalid_total_quota", "invalid_total_used_quota", "invalid_request_count",
"invalid_remain_quota", "invalid_used_quota", "invalid_quota_used",
]);
const configurationError = configurationCodes.has(error.message);
const errorCode = configurationError || responseCodes.has(error.message)
? error.message
: error.name === "TimeoutError"
? "timeout"
: error.name === "SyntaxError"
? "invalid_json"
: "network_or_runtime";
console.error(JSON.stringify({
event: "cometapi_usage_snapshot_failed",
category: configurationError ? "configuration" : "query",
errorCode,
httpStatus: error.httpStatus ?? null,
}));
process.exitCode = 1;
}
The normalize() function deliberately turns each returned key into an ordinal slot for general logs. Daily rows use the same in-memory name-to-slot map, so the event carries date-bounded per-key usage and request counts without copying names. A slot is scoped to one snapshot because the service does not document array ordering; do not treat it as a permanent key ID. The event also omits usernames, raw response text, request headers, and the constructed URL. If an access-controlled cost report needs the documented key names, store them in a separate restricted destination rather than adding them to a broadly searchable log stream. This separation is consistent with the OWASP Secrets Management Cheat Sheet
, which recommends least privilege, auditing, lifecycle controls, and avoiding plaintext secret exposure.
Who this is for
This pattern is for backend developers, platform engineers, and operators who already make CometAPI requests from Node.js and need a repeatable view of consumption. It works well as a scheduled worker, a deployment precheck, a command invoked by an operations runbook, or a small service that publishes metrics. It is intentionally not a browser or mobile pattern: a client distributed to users cannot keep a query credential private.
It is especially useful for teams with separate keys for production, staging, experiments, or different owners. The account-level totals answer “how much has the account used?” while the keys array and optional daily map help answer “which key and which dates explain the change?” The monitor reports the fields returned by CometAPI; it is not a replacement for a financial ledger or a new pricing calculator.
Before scheduling the job, create a dedicated credential for balance queries and apply the smallest practical positive limit in the dashboard when that control is available. The CometAPI guide recommends a dedicated credential and limiting exposure rather than sharing an unrestricted value with application traffic. For repository hygiene, pair this article with Keep CometAPI Keys Out of Tutorial Repositories .
Key takeaways
- Treat the documented query-service quota route as a monitoring route separate from model-generation routes.
- Supply the required credential at request time with
URLSearchParams; never paste it into source, a fixture, a screenshot, or a log message. - Use both optional dates when you need
daily_quota. Keep date values bounded and validate them before a network call. - Interpret
total_quotaas the documented current account balance in USD,total_used_quotaas cumulative usage in USD, andrequest_countas the account request total. - Keep
keysand daily entries in structured data, but redact or omit names from general telemetry. Preserve a documented-1quota sentinel as “unlimited,” not as zero. - Alert on stale snapshots and schema failures as well as low balances. A successful transport response is not necessarily a usable monitoring result.
A safe event shape can look like this, with illustrative zero values only:
{
"event": "cometapi_usage_snapshot",
"result": "ok",
"totalQuotaUsd": 0,
"totalUsedQuotaUsd": 0,
"requestCount": 0,
"entryCount": 0,
"entries": [],
"daily": []
}
The event contains operational measurements, not a credential. Add a snapshot timestamp and a retention policy in your own telemetry system. Do not add the URL query string to make troubleshooting “easier”; that turns an otherwise safe metric into a secret-bearing record.
Sources checked
The primary source is the CometAPI Query balance and usage documentation
. It defines the separate query-service host, the required credential field, optional date filters, account totals, per-key quota fields, and the conditions under which daily_quota appears. It also describes the -1 value for an unlimited per-key quota.
The implementation choice is supported by the Node.js global objects documentation
, which lists fetch among the available globals and describes it as browser-compatible. That lets a current Node.js runtime perform the HTTPS request without adding a client dependency just for this monitor.
The MDN URLSearchParams documentation
documents append(), iteration, serialization, and percent encoding. Those details matter here because the service contract needs a credential field and optional dates, while the monitor must avoid hand-built query strings and accidental logging of unencoded values.
Finally, the OWASP Secrets Management Cheat Sheet supplies the security context: use least privilege, centralize and audit secret handling, rotate and revoke credentials, use TLS, and keep secrets out of plaintext records. These are operational safeguards around the CometAPI contract, not claims about additional CometAPI response fields.
Contract details to verify
Verify the route and response shape during deployment against the linked CometAPI documentation. It uses a separate query service; do not substitute a model API base URL. The required credential is a query field in the service contract, and the example constructs it in memory without ever showing the resulting query string.
The optional start_date and end_date fields use YYYY-MM-DD. Send both when daily detail is needed. The service may return date keys with a timestamp suffix such as 2026-08-01T00:00:00Z; treat those keys as service-provided labels rather than assuming local midnight or a bare date. Keep the requested range explicit in the snapshot metadata.
At the top level, validate total_quota as the current USD balance, total_used_quota as cumulative USD usage, and request_count as an integer. Validate keys as an array. Each entry can contain name, remain_quota, and used_quota; a documented -1 means unlimited for a quota field. With a date range, inspect each daily_quota entry for token_name, quota_used, and request_count before turning it into a cost report.
Run two controlled checks before production. First, make a happy-path call for a short, known date range and compare the account totals with the dashboard. Second, run an error-path test in a non-production environment using an invalid date shape. Confirm that the process exits nonzero, emits a small failure event, and does not print the URL, query string, credential, or raw response. Then test a timeout or blocked network path and verify that the last successful snapshot is marked stale rather than silently reused as current.
Failure modes
Endpoint or runtime input missing. Stop before fetch when either environment value is absent, malformed, or outside the pinned HTTPS host and path. Classify this as configuration, alert the owner, and do not retry. A code such as endpoint_missing, endpoint_not_allowed, or runtime_input_missing is enough; neither value should appear in logs.
Incomplete or invalid date range. The service documents both dates for daily detail. Reject a range with only one date, an impossible calendar date, or a non-ISO shape before making the request. This prevents a malformed report from being mistaken for zero usage.
HTTP failure. A non-success status can indicate a bad credential, a policy problem, or a service response that needs investigation. Record only a status category and numeric status. Do not copy the response body into an exception or a support ticket. Decide whether a retry is appropriate from your own runbook; never retry a configuration error indefinitely.
Timeout or network outage. The abort timeout in the example bounds the worker. A scheduler may perform a small number of delayed attempts, but it should preserve the age of the last good snapshot and expose the outage. Do not overwrite a known value with zero merely because the latest request failed.
Unexpected JSON. A proxy, maintenance page, or contract change can return parseable JSON without the required fields. Reject missing numeric totals, non-array keys, and invalid types. Emit a schema-drift event without the body. This protects alerting from a false “healthy” result.
Unlimited sentinel. A remaining or used quota of -1 is documented as unlimited. Preserve that sentinel and present it explicitly. Converting it to zero would create a false exhaustion alert; converting it to a very large number would create a misleading budget chart.
Sensitive telemetry. HTTP access logs, debug middleware, shell history, and generic exception serializers can capture query strings. Disable full-URL logging for this worker, apply centralized redaction, and keep the dedicated credential out of command arguments. The related credential redaction patterns tutorial shows how to keep examples and diagnostics free of secret material.
Use this operator workflow for each scheduled run: check the last successful snapshot timestamp; run one bounded query; validate status and fields; compare account totals with the previous snapshot; inspect per-key slots and daily dates; classify any error as configuration, transport, or schema; and record the decision without copying the request URL. If exposure is suspected, revoke or rotate the dedicated credential through your established process before resuming monitoring.
FAQ
Is the balance route the same as a chat route?
No. The CometAPI documentation names a separate query service for account balance and usage. Keep its worker, timeout, access policy, and alert stream separate from model-generation traffic.
Why not put the credential directly in the URL string?
The service contract requires a query field, but hand-built strings are easy to copy into logs and easy to encode incorrectly. URLSearchParams.append() makes the value a request-time input and handles serialization. It does not make logging safe, so never print the resulting URL.
Do I get daily data without dates?
The documented daily_quota object is included when both start_date and end_date are supplied. Without both values, use the account totals and per-key fields, or request a bounded range when a daily breakdown is needed.
Should every application share one monitoring credential?
No. A dedicated, constrained credential reduces blast radius and makes ownership clearer. Separate environments or teams where practical, restrict which worker can read each value, and rotate or revoke it according to your secrets policy.
What belongs in an alert?
Include snapshot age, result category, HTTP status when present, account balance, usage change, request count, key-slot count, and date-window labels. Exclude the credential, query string, request headers, raw response, username, and full exception object unless an access-controlled system specifically requires a reviewed field.
Is total_quota a pricing estimate?
No. The CometAPI contract describes it as the current account balance in USD. Treat it as a monitoring measurement and reconcile financial decisions with the authoritative account and billing records.
Reader next step
Create a dedicated balance-query credential in your server runtime, run the example once with a short date range, and compare the sanitized event with the dashboard. Add a scheduler only after both the happy path and the invalid-date path behave as expected. Then set an alert for stale snapshots, low remaining quota, and schema rejection, with a retention period that fits your security policy. Before sharing the script, review Choose Reliable Storage Fields for CometAPI Text Results and remove any middleware that records full request URLs. Once the monitor is stable, document its owner, credential rotation date, and escalation path in the same restricted operations record.