Skip to content

Commit c6f1e43

Browse files
committed
refactor(billing): derive agent credit schema from tokenlens (DAT-100)
The credit rates in autumn.config.ts and the agent-cost-probe script were hand-picked magic numbers duplicated across two files, with only a cross-reference comment to keep them in sync. This factored the pricing into a single shared module that derives credits from real provider rates, plus adds the missing web_search credit to the probe. Formula: credit_per_token = usd_per_token × MARKUP × CREDITS_PER_USD - usd_per_token comes from tokenlens' Vercel AI Gateway catalog (anthropic/claude-4-sonnet) - MARKUP = 1.20 (20% margin) - CREDITS_PER_USD = 200 (tuning knob for plan-tier runway) Derived values exactly match the prior hand-picked schema: input 0.000_72 output 0.0036 cacheRead 0.000_072 cacheWrite 0.001_44 webSearch 5 (flat) cacheWrite override: tokenlens exposes Anthropic's 5-minute cache rate ($3.75/M), but the agent uses ttl: "1h" in prompt-cache.ts, which bills at the 1-hour rate ($6/M). The override lives alongside a comment explaining why — if the TTL ever changes, delete the constant and let tokenlens drive the rate directly. Changes: - packages/shared/src/billing/credit-schema.ts: new module exporting AGENT_CREDIT_SCHEMA, CREDITS_PER_USD, MARKUP, BASELINE_MODEL_ID, WEB_SEARCH_CREDIT_COST - packages/shared/package.json: add tokenlens dep + export path - apps/dashboard/autumn.config.ts: import AGENT_CREDIT_SCHEMA and feed its fields into the Autumn creditSchema array - apps/api/scripts/agent-cost-probe.ts: drop CURRENT_SCHEMA, import AGENT_CREDIT_SCHEMA, count web_search tool calls per turn and include them in the credit total (was being undercounted before)
1 parent 35dba4a commit c6f1e43

File tree

5 files changed

+164
-22
lines changed

5 files changed

+164
-22
lines changed

apps/api/scripts/agent-cost-probe.ts

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
* cache hits/misses are realistic.
1919
*/
2020

21+
import { AGENT_CREDIT_SCHEMA } from "@databuddy/shared/billing/credit-schema";
2122
import { randomUUIDv7 } from "bun";
2223
import { convertToModelMessages, ToolLoopAgent, type UIMessage } from "ai";
2324
import { createAgentConfig } from "../src/ai/agents";
@@ -58,24 +59,16 @@ if (!(websiteId && userId)) {
5859
process.exit(1);
5960
}
6061

61-
// Matches creditSchema in apps/dashboard/autumn.config.ts.
62-
// Keep in sync — if you change rates in one place, change them in the other.
63-
const CURRENT_SCHEMA = {
64-
input: 0.000_72,
65-
output: 0.0036,
66-
cacheRead: 0.000_072,
67-
cacheWrite: 0.001_44,
68-
};
69-
7062
function computeCredits(
71-
schema: typeof CURRENT_SCHEMA,
72-
s: ReturnType<typeof summarizeAgentUsage>
63+
s: ReturnType<typeof summarizeAgentUsage>,
64+
webSearchCalls: number
7365
): number {
7466
return (
75-
s.fresh_input_tokens * schema.input +
76-
s.output_tokens * schema.output +
77-
s.cache_read_tokens * schema.cacheRead +
78-
s.cache_write_tokens * schema.cacheWrite
67+
s.fresh_input_tokens * AGENT_CREDIT_SCHEMA.input +
68+
s.output_tokens * AGENT_CREDIT_SCHEMA.output +
69+
s.cache_read_tokens * AGENT_CREDIT_SCHEMA.cacheRead +
70+
s.cache_write_tokens * AGENT_CREDIT_SCHEMA.cacheWrite +
71+
webSearchCalls * AGENT_CREDIT_SCHEMA.webSearch
7972
);
8073
}
8174

@@ -117,6 +110,7 @@ async function main() {
117110
read: 0,
118111
write: 0,
119112
output: 0,
113+
webSearch: 0,
120114
};
121115

122116
for (const [idx, message] of messages.entries()) {
@@ -143,11 +137,15 @@ async function main() {
143137

144138
let assistantText = "";
145139
let toolCalls = 0;
140+
let webSearchCalls = 0;
146141
for await (const part of result.fullStream) {
147142
if (part.type === "text-delta") {
148143
assistantText += part.text ?? "";
149144
} else if (part.type === "tool-call") {
150145
toolCalls++;
146+
if (part.toolName === "web_search") {
147+
webSearchCalls++;
148+
}
151149
console.log(` → tool call: ${part.toolName}`);
152150
}
153151
}
@@ -162,16 +160,18 @@ async function main() {
162160
const steps = (await result.steps).length;
163161
const elapsed = ((Date.now() - t0) / 1000).toFixed(2);
164162
const summary = summarizeAgentUsage(modelNames.analytics, usage);
165-
const credits = computeCredits(CURRENT_SCHEMA, summary);
163+
const credits = computeCredits(summary, webSearchCalls);
166164

167165
totals.credits += credits;
168166
totals.fresh += summary.fresh_input_tokens;
169167
totals.read += summary.cache_read_tokens;
170168
totals.write += summary.cache_write_tokens;
171169
totals.output += summary.output_tokens;
170+
totals.webSearch += webSearchCalls;
172171

172+
const webSearchNote = webSearchCalls > 0 ? ` · web ${webSearchCalls}` : "";
173173
console.log(
174-
` ${elapsed}s · ${steps} steps · ${toolCalls} tools · fresh ${summary.fresh_input_tokens} · read ${summary.cache_read_tokens} · write ${summary.cache_write_tokens} · out ${summary.output_tokens}${
174+
` ${elapsed}s · ${steps} steps · ${toolCalls} tools${webSearchNote} · fresh ${summary.fresh_input_tokens} · read ${summary.cache_read_tokens} · write ${summary.cache_write_tokens} · out ${summary.output_tokens}${
175175
summary.reasoning_tokens > 0
176176
? ` (${summary.reasoning_tokens} reasoning)`
177177
: ""
@@ -189,6 +189,9 @@ async function main() {
189189
console.log(` cache read ${totals.read.toLocaleString().padStart(10)}`);
190190
console.log(` cache write ${totals.write.toLocaleString().padStart(10)}`);
191191
console.log(` output ${totals.output.toLocaleString().padStart(10)}`);
192+
console.log(
193+
` web search ${totals.webSearch.toLocaleString().padStart(10)}`
194+
);
192195
console.log(` credits ${totals.credits.toFixed(2).padStart(10)}`);
193196
console.log();
194197
console.log(

apps/dashboard/autumn.config.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AGENT_CREDIT_SCHEMA } from "@databuddy/shared/billing/credit-schema";
12
import { feature, item, plan } from "atmn";
23

34
/*
@@ -104,11 +105,26 @@ export const agent_credits = feature({
104105
name: "Agent Credits",
105106
type: "credit_system",
106107
creditSchema: [
107-
{ meteredFeatureId: "agent_input_tokens", creditCost: 0.000_72 },
108-
{ meteredFeatureId: "agent_output_tokens", creditCost: 0.0036 },
109-
{ meteredFeatureId: "agent_cache_read_tokens", creditCost: 0.000_072 },
110-
{ meteredFeatureId: "agent_cache_write_tokens", creditCost: 0.001_44 },
111-
{ meteredFeatureId: "agent_web_search_calls", creditCost: 5 },
108+
{
109+
meteredFeatureId: "agent_input_tokens",
110+
creditCost: AGENT_CREDIT_SCHEMA.input,
111+
},
112+
{
113+
meteredFeatureId: "agent_output_tokens",
114+
creditCost: AGENT_CREDIT_SCHEMA.output,
115+
},
116+
{
117+
meteredFeatureId: "agent_cache_read_tokens",
118+
creditCost: AGENT_CREDIT_SCHEMA.cacheRead,
119+
},
120+
{
121+
meteredFeatureId: "agent_cache_write_tokens",
122+
creditCost: AGENT_CREDIT_SCHEMA.cacheWrite,
123+
},
124+
{
125+
meteredFeatureId: "agent_web_search_calls",
126+
creditCost: AGENT_CREDIT_SCHEMA.webSearch,
127+
},
112128
],
113129
});
114130

bun.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/shared/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"./country-codes": "./src/country-codes.ts",
1010
"./crypto-utils": "./src/crypto-utils.ts",
1111

12+
"./billing/credit-schema": "./src/billing/credit-schema.ts",
1213
"./lists/filters": "./src/lists/filters.ts",
1314
"./lists/referrers": "./src/lists/referrers.ts",
1415
"./lists/timezones": "./src/lists/timezones.ts",
@@ -47,6 +48,7 @@
4748
"drizzle-orm": "catalog:",
4849
"evlog": "^2.8.0",
4950
"nanoid": "catalog:",
51+
"tokenlens": "^1.3.1",
5052
"ua-parser-js": "catalog:",
5153
"zod": "catalog:"
5254
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/**
2+
* Single source of truth for agent credit rates.
3+
*
4+
* Derives per-token credit costs from tokenlens' Vercel AI Gateway catalog
5+
* plus a business markup, so the Autumn creditSchema in autumn.config.ts
6+
* and the agent-cost-probe script stay in sync with provider prices
7+
* automatically. The raw USD rates come from tokenlens; the markup and
8+
* credit-to-USD ratio are the pricing knobs we tune per plan tier.
9+
*
10+
* Credit formula: credits_per_token = usd_per_token × MARKUP × CREDITS_PER_USD
11+
*/
12+
13+
import { vercelModels } from "tokenlens/providers/vercel";
14+
15+
/**
16+
* How many credits the user spends per USD of underlying provider cost.
17+
* Tunes plan-tier runway (free 500 / hobby 2500 / pro 25000). Raising
18+
* this makes the same dollar of provider usage burn more credits.
19+
*/
20+
export const CREDITS_PER_USD = 200;
21+
22+
/** Business markup on top of provider cost. 1.20 = 20% margin. */
23+
export const MARKUP = 1.2;
24+
25+
/**
26+
* Model whose provider rates back the credit schema. If the agent uses
27+
* multiple models with materially different prices, pick the most
28+
* expensive as the ceiling — we'd rather slightly over-charge than
29+
* lose margin on the pricier model.
30+
*/
31+
export const BASELINE_MODEL_ID = "anthropic/claude-4-sonnet" as const;
32+
33+
/**
34+
* Anthropic's 1-hour prompt cache write rate (USD per 1M tokens).
35+
*
36+
* The Vercel AI Gateway catalog in tokenlens exposes Anthropic's
37+
* 5-minute cache rate ($3.75/M), but our agent is configured with
38+
* `ttl: "1h"` in apps/api/src/ai/config/prompt-cache.ts so Anthropic
39+
* bills the 1-hour rate ($6/M). We override cacheWrite here to match
40+
* production billing. If the agent switches back to 5-minute TTL,
41+
* delete this constant and let tokenlens drive the rate directly.
42+
*/
43+
const CACHE_WRITE_1H_USD_PER_M_TOKENS = 6;
44+
45+
/**
46+
* Flat credit cost per agent_web_search_calls. 5 credits ≈ $0.025 at
47+
* CREDITS_PER_USD=200 — priced separately from token burn because the
48+
* Perplexity call is a fixed-cost API hit regardless of tokens returned.
49+
*/
50+
export const WEB_SEARCH_CREDIT_COST = 5;
51+
52+
const TOKENS_PER_MILLION = 1_000_000;
53+
54+
interface ModelCostsPerMillion {
55+
cache_read: number;
56+
cache_write: number;
57+
input: number;
58+
output: number;
59+
}
60+
61+
function getBaselineUsdPerMillion(): ModelCostsPerMillion {
62+
const model = vercelModels.models[BASELINE_MODEL_ID];
63+
if (!model?.cost) {
64+
throw new Error(
65+
`tokenlens vercelModels is missing cost for ${BASELINE_MODEL_ID}`
66+
);
67+
}
68+
return {
69+
input: model.cost.input,
70+
output: model.cost.output,
71+
cache_read: model.cost.cache_read,
72+
// Override with the 1-hour rate — see CACHE_WRITE_1H_USD_PER_M_TOKENS.
73+
cache_write: CACHE_WRITE_1H_USD_PER_M_TOKENS,
74+
};
75+
}
76+
77+
/**
78+
* Converts a per-million-tokens USD rate into a per-token credit rate.
79+
* Rounds to 12 significant digits to avoid floating-point noise like
80+
* `0.0007199999999999999` leaking into the Autumn creditSchema payload.
81+
*/
82+
function toCredits(usdPerMillion: number): number {
83+
const raw = (usdPerMillion / TOKENS_PER_MILLION) * MARKUP * CREDITS_PER_USD;
84+
return Number.parseFloat(raw.toPrecision(12));
85+
}
86+
87+
export interface AgentCreditSchema {
88+
/** Credits per cache-read input token. */
89+
cacheRead: number;
90+
/** Credits per cache-write input token. */
91+
cacheWrite: number;
92+
/** Credits per fresh (non-cached) input token. */
93+
input: number;
94+
/** Credits per output token. */
95+
output: number;
96+
/** Flat credits per web search call. */
97+
webSearch: number;
98+
}
99+
100+
const baselineUsd = getBaselineUsdPerMillion();
101+
102+
/**
103+
* Canonical agent credit schema. Import this from autumn.config.ts and
104+
* from any cost probe so both stay in lockstep.
105+
*/
106+
export const AGENT_CREDIT_SCHEMA: AgentCreditSchema = {
107+
input: toCredits(baselineUsd.input),
108+
output: toCredits(baselineUsd.output),
109+
cacheRead: toCredits(baselineUsd.cache_read),
110+
cacheWrite: toCredits(baselineUsd.cache_write),
111+
webSearch: WEB_SEARCH_CREDIT_COST,
112+
};

0 commit comments

Comments
 (0)