Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 48 additions & 16 deletions src/pricing/tier-engine.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,75 @@
/**
* Tiered Pricing Engine — Issue #1
* Implements 4-tier pricing for x402 API calls
* Implements 4-tier pricing for x402 API calls.
*
* Acceptance surface includes both camelCase and snake_case entry points:
* `getTierPrice` / `get_tier_price(call_count, priority_flag)`.
*/

export type Tier = 'free' | 'standard' | 'premium' | 'priority';
export type Tier = "free" | "standard" | "premium" | "priority";

export interface TierResult {
tier: Tier;
pricePerCall: number; // in USDC
callsInTier: number;
}

function normalizeCallCount(callCount: number): number {
if (!Number.isFinite(callCount)) {
throw new RangeError("callCount must be a finite number");
}
// Treat non-positive counts as "first call" so free tier applies.
if (callCount < 1) return 1;
return Math.floor(callCount);
}

/**
* Returns the price per call based on total call count and priority flag.
* - Tier 1 (Free): calls 1–50 → $0.00
* - Tier 2 (Standard): calls 51–500 → $0.01
* - Tier 3 (Premium): calls 500+ → $0.03
* - Tier 4 (Priority): priority=true → $0.10
* - Tier 2 (Standard): calls 51–500 → $0.01
* - Tier 3 (Premium): calls 501+ → $0.03
* - Tier 4 (Priority): priority=true → $0.10 (overrides count tiers)
*/
export function getTierPrice(callCount: number, priorityFlag = false): TierResult {
export function getTierPrice(
callCount: number,
priorityFlag = false,
): TierResult {
const n = normalizeCallCount(callCount);
if (priorityFlag) {
return { tier: 'priority', pricePerCall: 0.10, callsInTier: 1 };
return { tier: "priority", pricePerCall: 0.1, callsInTier: 1 };
}
if (callCount <= 50) {
return { tier: 'free', pricePerCall: 0.00, callsInTier: 50 - callCount + 1 };
if (n <= 50) {
return { tier: "free", pricePerCall: 0.0, callsInTier: 50 - n + 1 };
}
if (callCount <= 500) {
return { tier: 'standard', pricePerCall: 0.01, callsInTier: 500 - callCount + 1 };
if (n <= 500) {
return { tier: "standard", pricePerCall: 0.01, callsInTier: 500 - n + 1 };
}
return { tier: 'premium', pricePerCall: 0.03, callsInTier: Infinity };
return { tier: "premium", pricePerCall: 0.03, callsInTier: Infinity };
}

/** Snake_case alias required by issue #1 acceptance criteria. */
export function get_tier_price(
call_count: number,
priority_flag = false,
): TierResult {
return getTierPrice(call_count, priority_flag);
}

/**
* Calculates total cost for a batch of calls.
* Calculates total cost for a batch of calls starting at `startCount`.
*/
export function calculateBatchCost(startCount: number, numCalls: number, priority = false): number {
export function calculateBatchCost(
startCount: number,
numCalls: number,
priority = false,
): number {
if (!Number.isFinite(numCalls) || numCalls < 0) {
throw new RangeError("numCalls must be a non-negative finite number");
}
let total = 0;
for (let i = 0; i < numCalls; i++) {
const n = Math.floor(numCalls);
for (let i = 0; i < n; i++) {
total += getTierPrice(startCount + i, priority).pricePerCall;
}
return Math.round(total * 1e6) / 1e6; // round to 6 decimals (USDC precision)
return Math.round(total * 1e6) / 1e6; // USDC precision
}
61 changes: 44 additions & 17 deletions tests/pricing.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,58 @@
import { assertEquals } from 'https://deno.land/std@0.224.0/assert/mod.ts';
import { getTierPrice, calculateBatchCost } from '../src/pricing/tier-engine.ts';
import { assertEquals, assertThrows } from "https://deno.land/std@0.224.0/assert/mod.ts";
import {
calculateBatchCost,
get_tier_price,
getTierPrice,
} from "../src/pricing/tier-engine.ts";

Deno.test('Tier 1: free for first 50 calls', () => {
assertEquals(getTierPrice(1).tier, 'free');
assertEquals(getTierPrice(50).tier, 'free');
assertEquals(getTierPrice(1).pricePerCall, 0.00);
Deno.test("Tier 1: free for first 50 calls", () => {
assertEquals(getTierPrice(1).tier, "free");
assertEquals(getTierPrice(50).tier, "free");
assertEquals(getTierPrice(1).pricePerCall, 0.0);
assertEquals(getTierPrice(50).pricePerCall, 0.0);
});

Deno.test('Tier 2: standard for calls 51-500', () => {
assertEquals(getTierPrice(51).tier, 'standard');
assertEquals(getTierPrice(500).tier, 'standard');
Deno.test("Tier 2: standard for calls 51-500", () => {
assertEquals(getTierPrice(51).tier, "standard");
assertEquals(getTierPrice(500).tier, "standard");
assertEquals(getTierPrice(51).pricePerCall, 0.01);
assertEquals(getTierPrice(500).pricePerCall, 0.01);
});

Deno.test('Tier 3: premium for calls 500+', () => {
assertEquals(getTierPrice(501).tier, 'premium');
Deno.test("Tier 3: premium for calls 501+", () => {
assertEquals(getTierPrice(501).tier, "premium");
assertEquals(getTierPrice(10_000).tier, "premium");
assertEquals(getTierPrice(501).pricePerCall, 0.03);
});

Deno.test('Tier 4: priority flag overrides all', () => {
assertEquals(getTierPrice(1, true).tier, 'priority');
assertEquals(getTierPrice(1000, true).pricePerCall, 0.10);
Deno.test("Tier 4: priority flag overrides all count tiers", () => {
assertEquals(getTierPrice(1, true).tier, "priority");
assertEquals(getTierPrice(1000, true).pricePerCall, 0.1);
assertEquals(get_tier_price(25, true).tier, "priority");
});

Deno.test('Batch cost calculation', () => {
// 10 free calls = $0
Deno.test("snake_case get_tier_price matches getTierPrice", () => {
for (const n of [1, 50, 51, 500, 501]) {
assertEquals(get_tier_price(n), getTierPrice(n));
assertEquals(get_tier_price(n, true), getTierPrice(n, true));
}
});

Deno.test("boundaries: non-positive callCount clamps into free tier", () => {
assertEquals(getTierPrice(0).tier, "free");
assertEquals(getTierPrice(-3).tier, "free");
assertEquals(get_tier_price(0).pricePerCall, 0);
});

Deno.test("invalid callCount throws", () => {
assertThrows(() => getTierPrice(Number.NaN), RangeError);
assertThrows(() => get_tier_price(Number.POSITIVE_INFINITY), RangeError);
});

Deno.test("Batch cost calculation", () => {
assertEquals(calculateBatchCost(1, 10), 0);
// 1 standard call
assertEquals(calculateBatchCost(51, 1), 0.01);
// 2 free + 2 standard across the 50→51 boundary
assertEquals(calculateBatchCost(49, 4), 0.02);
assertEquals(calculateBatchCost(1, 3, true), 0.3);
});