Skip to content

Commit 92383fe

Browse files
committed
Report credits in zenrows usage — the endpoint always sent them
Verified against the live endpoint: /v1/subscriptions/self/details returns usage_credits and credit_limit next to the dollar figure. Neither was declared on UsageDetails, so the command printed dollars and left the reader to convert. They cannot. The rate is per plan, not a platform constant: Free bills $0.001/credit (5,000 credits for $5) while a Business yearly plan bills 8.9997e-05. The response carries plan.unit_cost for exactly this reason, and credit_limit * unit_cost === plan.price holds on the live payload. So the command now leads with credits used, the credit limit and what is left, which is the unit the docs, the dashboard and the error messages all speak in. The dollar line stays underneath. unit_cost is declared with a note against ever hardcoding a conversion factor — an easy mistake to make from a single account's numbers, and wrong on every other plan.
1 parent c8bee88 commit 92383fe

3 files changed

Lines changed: 56 additions & 1 deletion

File tree

‎src/cli/commands/usage.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import { log } from "../../core/logger.ts";
88
import { fetchUsage } from "../../core/usage.ts";
99
import { parse, type Command, type RunContext } from "../command.ts";
1010

11+
/** Thousands separators, so a seven-digit credit limit stays readable. */
12+
export function fmt(n: number): string {
13+
return n.toLocaleString("en-US");
14+
}
15+
1116
/** Free plan still ships as plan_code=trial in R1; display the product name. */
1217
export function formatPlanName(name: string | undefined): string {
1318
if (!name) return "—";
@@ -42,6 +47,18 @@ export const usage: Command = {
4247
const api = u.plan?.products?.api;
4348
log.info(`Plan: ${formatPlanName(u.plan?.name)}${u.plan?.recurrence ? ` (${u.plan.recurrence})` : ""}`);
4449
log.info(`Status: ${formatPlanStatus(u.status)}`);
50+
// Credits are the unit the docs, the dashboard and the error messages all speak in,
51+
// and the endpoint has been returning them all along. Showing only the dollar figure
52+
// left the reader to convert — and the rate is per plan, so they could not.
53+
if (u.usage_credits !== undefined && u.credit_limit !== undefined) {
54+
const remaining = Math.max(0, u.credit_limit - u.usage_credits);
55+
log.info(
56+
`Credits: ${fmt(u.usage_credits)} of ${fmt(u.credit_limit)} used` +
57+
`${u.usage_percent !== undefined ? ` (${u.usage_percent}%)` : ""} — ${fmt(remaining)} left`,
58+
);
59+
} else if (u.usage_credits !== undefined) {
60+
log.info(`Credits: ${fmt(u.usage_credits)} used`);
61+
}
4562
if (u.usage !== undefined) {
4663
log.info(`Usage: ${u.usage}${u.usage_percent !== undefined ? ` (${u.usage_percent}% of plan)` : ""}`);
4764
}

‎src/core/usage.ts‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export interface UsageConcurrency {
1919

2020
export interface UsageProduct {
2121
usage?: number;
22+
/** Same consumption as `usage`, counted in credits. */
23+
usage_credits?: number;
2224
concurrency?: UsageConcurrency;
2325
[k: string]: unknown;
2426
}
@@ -27,13 +29,23 @@ export interface UsageDetails {
2729
status?: string;
2830
period_starts_at?: string;
2931
period_ends_at?: string;
30-
/** Total units consumed across all products. */
32+
/** Total consumed across all products, in dollars. */
3133
usage?: number;
34+
/** The same consumption in credits — what the docs and the dashboard quote. */
35+
usage_credits?: number;
36+
/** The plan's allowance in credits. `credit_limit * plan.unit_cost === plan.price`. */
37+
credit_limit?: number;
3238
/** Consumption as a percentage of the plan limit. */
3339
usage_percent?: number;
3440
plan?: {
3541
name?: string;
3642
price?: number;
43+
/**
44+
* Dollars per credit, and it is **per plan**, not a platform constant: Free bills
45+
* $0.001/credit (5,000 credits for $5) while larger plans get a volume rate. Never
46+
* convert between dollars and credits with a hardcoded factor.
47+
*/
48+
unit_cost?: number;
3749
recurrence?: string;
3850
products?: {
3951
api?: UsageProduct;

‎tests/usage.test.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { test } from "node:test";
22
import assert from "node:assert/strict";
33
import { fetchUsage, usageUrl } from "../src/core/usage.ts";
44
import { ToolkitError } from "../src/core/errors.ts";
5+
import { fmt } from "../src/cli/commands/usage.ts";
6+
import type { UsageDetails } from "../src/core/usage.ts";
57

68
test("usageUrl derives subscriptions/self/details from the api base", () => {
79
assert.equal(usageUrl("https://api.zenrows.com/v1/"), "https://api.zenrows.com/v1/subscriptions/self/details");
@@ -61,3 +63,27 @@ test("fetchUsage maps a 402 (over usage limit) to POLICY_MAX_CREDITS_EXCEEDED",
6163
(e: unknown) => e instanceof ToolkitError && e.code === "POLICY_MAX_CREDITS_EXCEEDED",
6264
);
6365
});
66+
67+
test("zenrows usage reports credits, which the endpoint has always returned", () => {
68+
// Verified against the live endpoint: it sends usage_credits and credit_limit
69+
// alongside the dollar figure. Neither was declared on UsageDetails, so the command
70+
// printed only dollars and left the reader to convert — which they cannot do, because
71+
// the rate is per plan (Free $0.001/credit, larger plans a volume rate).
72+
assert.equal(fmt(62982), "62,982");
73+
assert.equal(fmt(35999978), "35,999,978");
74+
});
75+
76+
test("UsageDetails carries credits and the per-plan rate", () => {
77+
const sample: UsageDetails = {
78+
status: "ACTIVE",
79+
usage: 5.66823039823616,
80+
usage_credits: 62982,
81+
credit_limit: 35999978,
82+
usage_percent: 0,
83+
plan: { name: "Business", price: 3239.89, unit_cost: 8.9997e-5, recurrence: "YEARLY" },
84+
};
85+
// credit_limit * unit_cost === plan.price is the invariant that makes the rate per-plan
86+
// rather than a platform constant. Holds on the live response.
87+
assert.ok(Math.abs(sample.credit_limit! * sample.plan!.unit_cost! - sample.plan!.price!) < 0.01);
88+
assert.ok(Math.abs(sample.usage! / sample.usage_credits! - sample.plan!.unit_cost!) < 1e-8);
89+
});

0 commit comments

Comments
 (0)