From 0571e1c2f2b38f747d9139dd792f8fb7a7aa2cb8 Mon Sep 17 00:00:00 2001 From: Devyash Saini Date: Thu, 4 Jun 2026 00:12:30 +0530 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20biller.expr()=20accepts=20number=20?= =?UTF-8?q?and=20TagExpr=20=E2=80=94=20unified=20expression=20entry=20poin?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/basic-usage-expr.ts | 11 ++++++++++ packages/scrawn/src/core/scrawn.ts | 35 +++++++++++++++++------------- 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/examples/basic-usage-expr.ts b/examples/basic-usage-expr.ts index 1cd5963..5684483 100644 --- a/examples/basic-usage-expr.ts +++ b/examples/basic-usage-expr.ts @@ -19,6 +19,17 @@ async function main() { debit: add(biller.expr("COMPLEX_FEE"), mul(biller.tag("PREMIUM_CALL"), 5)), }); + // biller.expr() also accepts raw amounts and tags + await biller.basicUsageEventConsumer({ + userId: "c0971bcb-b901-4c3e-a191-c9a97871c39f", + debit: biller.expr(250), + }); + + await biller.basicUsageEventConsumer({ + userId: "c0971bcb-b901-4c3e-a191-c9a97871c39f", + debit: biller.expr(biller.tag("EXTRA_FEE")), + }); + console.log("Basic usage expression events consumed successfully"); } diff --git a/packages/scrawn/src/core/scrawn.ts b/packages/scrawn/src/core/scrawn.ts index 12a75e4..49abe61 100644 --- a/packages/scrawn/src/core/scrawn.ts +++ b/packages/scrawn/src/core/scrawn.ts @@ -305,39 +305,44 @@ export class Scrawn< } /** - * Create a type-safe reference to a persisted expression. + * Create a type-safe reference to a persisted expression, inline expression, + * tag, or raw amount. All expressions should go through `biller.expr()`. * * Expression names are compile-time checked against known expressions * synced from the Scrawn server. The backend resolves the stored * expression string and evaluates it at runtime. * - * Also accepts inline PriceExpr as a passthrough for a consistent - * `biller.expr()` entry point for all expressions. + * Also accepts inline PriceExpr, TagExpr, and raw numbers as a unified + * `biller.expr()` entry point. * - * @param nameOrExpr - The persisted expression name or an inline PriceExpr - * @returns An ExprRef (if name) or the original PriceExpr (passthrough) + * @param value - A persisted expression name, inline PriceExpr, TagExpr, or raw number + * @returns A PriceExpr representing the expression * * @example * ```typescript * // Reference a persisted expression - * biller.basicUsageEventConsumer({ - * userId: 'u123', - * debitExpr: biller.expr("MY_EXPR"), - * }); + * biller.expr("MY_EXPR") * - * // Inline expression passthrough - * biller.basicUsageEventConsumer({ - * userId: 'u123', - * debitExpr: biller.expr(mul(biller.tag("PREMIUM_CALL"), 3)), - * }); + * // Inline expression + * biller.expr(mul(biller.tag("PREMIUM_CALL"), 3)) + * + * // Wrap a tag + * biller.expr(biller.tag("EXTRA_FEE")) + * + * // Wrap a raw amount + * biller.expr(250) * ``` */ + expr(amount: number): PriceExpr; expr(name: T): PriceExpr; expr(expr: PriceExpr): PriceExpr; - expr(value: string | PriceExpr): PriceExpr { + expr(value: string | number | PriceExpr): PriceExpr { if (typeof value === "string") { return { kind: "exprRef", name: value } as PriceExpr; } + if (typeof value === "number") { + return { kind: "amount", value } as PriceExpr; + } return value; } From 618d4a362faa792ff049435e877c1503feced801 Mon Sep 17 00:00:00 2001 From: Devyash Saini Date: Thu, 4 Jun 2026 00:18:53 +0530 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20branded=20ExprValue=20type=20?= =?UTF-8?q?=E2=80=94=20inline=20expressions=20require=20biller.expr(),=20r?= =?UTF-8?q?aw=20number=20and=20tag=20still=20work=20directly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/scrawn/src/core/pricing/index.ts | 1 + packages/scrawn/src/core/pricing/types.ts | 18 ++++++++++++++++++ packages/scrawn/src/core/scrawn.ts | 22 ++++++++++++++-------- packages/scrawn/src/core/types/event.ts | 7 +++++-- 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/packages/scrawn/src/core/pricing/index.ts b/packages/scrawn/src/core/pricing/index.ts index 211e245..4dfa885 100644 --- a/packages/scrawn/src/core/pricing/index.ts +++ b/packages/scrawn/src/core/pricing/index.ts @@ -32,6 +32,7 @@ export type { OutputTokensExpr, ExprRef, PriceExpr, + ExprValue, ExprInput, } from "./types.js"; diff --git a/packages/scrawn/src/core/pricing/types.ts b/packages/scrawn/src/core/pricing/types.ts index af1e2bd..be77a45 100644 --- a/packages/scrawn/src/core/pricing/types.ts +++ b/packages/scrawn/src/core/pricing/types.ts @@ -96,6 +96,24 @@ export type PriceExpr = | OutputTokensExpr | ExprRef; +/** + * A branded PriceExpr returned by biller.expr(). + * Use biller.expr() to wrap inline expressions, tags, or amounts. + * Raw OpExpr values (e.g., mul(tag("X"), 3)) are not accepted by debit + * — they must be wrapped in biller.expr(). + */ +declare const EXPR_BRAND: unique symbol; +export type ExprValue = PriceExpr & { + readonly [EXPR_BRAND]: true; +}; + +/** @internal — casts a PriceExpr to ExprValue at compile time, no-op at runtime. */ +export function toExprValue( + expr: PriceExpr +): ExprValue { + return expr as ExprValue; +} + /** * Input type for DSL builder functions. * Accepts either a PriceExpr or a raw number (interpreted as cents). diff --git a/packages/scrawn/src/core/scrawn.ts b/packages/scrawn/src/core/scrawn.ts index 49abe61..fe0274e 100644 --- a/packages/scrawn/src/core/scrawn.ts +++ b/packages/scrawn/src/core/scrawn.ts @@ -15,7 +15,13 @@ import type { AuthMethodName, AllCredentials, } from "./types/auth.js"; -import type { TagExpr, PriceExpr, ExprRef } from "./pricing/types.js"; +import type { + TagExpr, + PriceExpr, + ExprRef, + ExprValue, +} from "./pricing/types.js"; +import { toExprValue } from "./pricing/types.js"; import { ApiKeyAuth } from "./auth/apiKeyAuth.js"; import { ScrawnLogger } from "../utils/logger.js"; import { matchPath } from "../utils/pathMatcher.js"; @@ -333,17 +339,17 @@ export class Scrawn< * biller.expr(250) * ``` */ - expr(amount: number): PriceExpr; - expr(name: T): PriceExpr; - expr(expr: PriceExpr): PriceExpr; - expr(value: string | number | PriceExpr): PriceExpr { + expr(amount: number): ExprValue; + expr(name: T): ExprValue; + expr(expr: PriceExpr): ExprValue; + expr(value: string | number | PriceExpr): ExprValue { if (typeof value === "string") { - return { kind: "exprRef", name: value } as PriceExpr; + return toExprValue({ kind: "exprRef", name: value } as PriceExpr); } if (typeof value === "number") { - return { kind: "amount", value } as PriceExpr; + return toExprValue({ kind: "amount", value } as PriceExpr); } - return value; + return toExprValue(value); } /** diff --git a/packages/scrawn/src/core/types/event.ts b/packages/scrawn/src/core/types/event.ts index c32fc68..365de56 100644 --- a/packages/scrawn/src/core/types/event.ts +++ b/packages/scrawn/src/core/types/event.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import type { PriceExpr } from "../pricing/types.js"; +import type { PriceExpr, TagExpr, ExprValue } from "../pricing/types.js"; import type { ScrawnError } from "../errors/index.js"; import { isValidExpr, containsTokenExpr } from "../pricing/validate.js"; @@ -122,7 +122,10 @@ export const EventPayloadSchema = z.object({ * debit: biller.expr("SAVED_RATE") * ``` */ -export type Debit = number | PriceExpr; +export type Debit = + | number + | TagExpr + | ExprValue; /** * Payload structure for event tracking. From 53075d1841501a7d4debe1062c1923001078533e Mon Sep 17 00:00:00 2001 From: Devyash Saini Date: Thu, 4 Jun 2026 00:19:50 +0530 Subject: [PATCH 3/3] fix: wrap inline expressions with biller.expr() in all examples --- examples/ai-sdk-wrapper-usage.ts | 2 +- examples/ai-token-stream-expr-usage.ts | 6 +++--- examples/basic-usage-expr.ts | 8 +++++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/examples/ai-sdk-wrapper-usage.ts b/examples/ai-sdk-wrapper-usage.ts index 4073ade..0e0a130 100644 --- a/examples/ai-sdk-wrapper-usage.ts +++ b/examples/ai-sdk-wrapper-usage.ts @@ -25,7 +25,7 @@ async function main() { userId: "c0971bcb-b901-4c3e-a191-c9a97871c39f", event, inputDebit: biller.tag("PREMIUM_CALL"), - outputDebit: mul(outputTokens(), 0.0001), + outputDebit: biller.expr(mul(outputTokens(), 0.0001)), }); }, }); diff --git a/examples/ai-token-stream-expr-usage.ts b/examples/ai-token-stream-expr-usage.ts index cbbae71..6683498 100644 --- a/examples/ai-token-stream-expr-usage.ts +++ b/examples/ai-token-stream-expr-usage.ts @@ -19,7 +19,7 @@ async function* tokenUsageFromAIStream(): AsyncGenerator< inputTokens: 150, outputTokens: 0, inputDebit: biller.expr("PER_TOKEN_INPUT"), - outputDebit: mul(biller.tag("EXTRA_FEE"), outputTokens()), + outputDebit: biller.expr(mul(biller.tag("EXTRA_FEE"), outputTokens())), }; yield { @@ -27,8 +27,8 @@ async function* tokenUsageFromAIStream(): AsyncGenerator< model: "gpt-4", inputTokens: 0, outputTokens: 75, - inputDebit: mul(biller.tag("PREMIUM_CALL"), inputTokens()), - outputDebit: mul(biller.tag("EXTRA_FEE"), outputTokens()), + inputDebit: biller.expr(mul(biller.tag("PREMIUM_CALL"), inputTokens())), + outputDebit: biller.expr(mul(biller.tag("EXTRA_FEE"), outputTokens())), }; } diff --git a/examples/basic-usage-expr.ts b/examples/basic-usage-expr.ts index 5684483..14e6503 100644 --- a/examples/basic-usage-expr.ts +++ b/examples/basic-usage-expr.ts @@ -6,17 +6,19 @@ config({ path: ".env.local" }); async function main() { await biller.basicUsageEventConsumer({ userId: "c0971bcb-b901-4c3e-a191-c9a97871c39f", - debit: mul(biller.tag("PREMIUM_CALL"), 3), + debit: biller.expr(mul(biller.tag("PREMIUM_CALL"), 3)), }); await biller.basicUsageEventConsumer({ userId: "c0971bcb-b901-4c3e-a191-c9a97871c39f", - debit: mul(biller.tag("EXTRA_FEE"), 3), + debit: biller.expr(mul(biller.tag("EXTRA_FEE"), 3)), }); await biller.basicUsageEventConsumer({ userId: "c0971bcb-b901-4c3e-a191-c9a97871c39f", - debit: add(biller.expr("COMPLEX_FEE"), mul(biller.tag("PREMIUM_CALL"), 5)), + debit: biller.expr( + add(biller.expr("COMPLEX_FEE"), mul(biller.tag("PREMIUM_CALL"), 5)) + ), }); // biller.expr() also accepts raw amounts and tags