@umpledger/sdk 2.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +839 -0
- package/dist/index.d.ts +839 -0
- package/dist/index.js +1520 -0
- package/dist/index.mjs +1466 -0
- package/examples/multi-agent-marketplace.ts +140 -0
- package/examples/quickstart.ts +107 -0
- package/package.json +58 -0
- package/src/core/agent-manager.ts +200 -0
- package/src/core/audit-trail.ts +91 -0
- package/src/core/index.ts +3 -0
- package/src/core/wallet-manager.ts +257 -0
- package/src/index.ts +188 -0
- package/src/pricing/engine.ts +311 -0
- package/src/pricing/index.ts +3 -0
- package/src/pricing/templates.ts +237 -0
- package/src/settlement/bus.ts +253 -0
- package/src/settlement/index.ts +2 -0
- package/src/terms/contract-manager.ts +142 -0
- package/src/terms/index.ts +2 -0
- package/src/terms/metering.ts +106 -0
- package/src/types.ts +417 -0
- package/src/utils/errors.ts +91 -0
- package/src/utils/id.ts +35 -0
- package/src/utils/index.ts +2 -0
- package/tests/ump.test.ts +525 -0
- package/tsconfig.json +28 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PricingRule, FixedRule, UnitRateRule, TieredRule,
|
|
3
|
+
PercentageRule, ThresholdRule, TimeWindowRule,
|
|
4
|
+
ConditionalRule, CompositeRule, UsageEvent, RatedRecord,
|
|
5
|
+
} from '../types';
|
|
6
|
+
import { generateId, round, hrTimestamp } from '../utils/id';
|
|
7
|
+
import { UMPError } from '../utils/errors';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Context passed into the rating engine for each usage event.
|
|
11
|
+
* Contains the event data plus any external dimensions needed
|
|
12
|
+
* for conditional evaluation.
|
|
13
|
+
*/
|
|
14
|
+
export interface RatingContext {
|
|
15
|
+
event: UsageEvent;
|
|
16
|
+
cumulativeQuantity?: number; // for tiered calculations within a period
|
|
17
|
+
referenceAmount?: number; // for percentage-based calculations
|
|
18
|
+
currentTime?: Date; // override for time-window testing
|
|
19
|
+
attributes?: Record<string, string | number | boolean>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* PricingEngine — Layer 2 primitive
|
|
24
|
+
*
|
|
25
|
+
* Evaluates the 8 atomic pricing primitives and their compositions.
|
|
26
|
+
* Takes usage events + pricing rules → rated records (billable amounts).
|
|
27
|
+
*/
|
|
28
|
+
export class PricingEngine {
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Rate a single usage event against a pricing rule.
|
|
32
|
+
* Returns the calculated amount.
|
|
33
|
+
*/
|
|
34
|
+
rate(rule: PricingRule, context: RatingContext): RatedRecord {
|
|
35
|
+
const amount = this.evaluate(rule, context);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
ratedRecordId: generateId('rat'),
|
|
39
|
+
usageEventId: context.event.eventId,
|
|
40
|
+
contractId: context.event.contractId,
|
|
41
|
+
pricingRuleId: rule.ruleId,
|
|
42
|
+
quantity: context.event.quantity,
|
|
43
|
+
rate: context.event.quantity > 0 ? round(amount / context.event.quantity, 6) : 0,
|
|
44
|
+
amount: round(amount),
|
|
45
|
+
currency: 'USD', // default, override via contract
|
|
46
|
+
ratedAt: hrTimestamp(),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Rate multiple usage events against a pricing rule.
|
|
52
|
+
*/
|
|
53
|
+
rateBatch(rule: PricingRule, events: UsageEvent[]): RatedRecord[] {
|
|
54
|
+
let cumulative = 0;
|
|
55
|
+
return events.map(event => {
|
|
56
|
+
cumulative += event.quantity;
|
|
57
|
+
const context: RatingContext = {
|
|
58
|
+
event,
|
|
59
|
+
cumulativeQuantity: cumulative,
|
|
60
|
+
};
|
|
61
|
+
return this.rate(rule, context);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Simulate: calculate projected cost without creating rated records.
|
|
67
|
+
*/
|
|
68
|
+
simulate(rule: PricingRule, quantity: number, dimensions?: Record<string, string | number>): number {
|
|
69
|
+
const mockEvent: UsageEvent = {
|
|
70
|
+
eventId: 'sim',
|
|
71
|
+
sourceAgentId: 'sim',
|
|
72
|
+
targetAgentId: 'sim',
|
|
73
|
+
contractId: 'sim',
|
|
74
|
+
serviceId: 'sim',
|
|
75
|
+
timestamp: new Date(),
|
|
76
|
+
quantity,
|
|
77
|
+
unit: 'UNIT',
|
|
78
|
+
dimensions: dimensions || {},
|
|
79
|
+
};
|
|
80
|
+
return round(this.evaluate(rule, { event: mockEvent }));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Human-readable breakdown of how a price was calculated.
|
|
85
|
+
*/
|
|
86
|
+
explain(rule: PricingRule, context: RatingContext): string {
|
|
87
|
+
return this.buildExplanation(rule, context, 0);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ═══════════════════════════════════════════════════════════
|
|
91
|
+
// PRIMITIVE EVALUATORS
|
|
92
|
+
// ═══════════════════════════════════════════════════════════
|
|
93
|
+
|
|
94
|
+
private evaluate(rule: PricingRule, ctx: RatingContext): number {
|
|
95
|
+
switch (rule.primitive) {
|
|
96
|
+
case 'FIXED': return this.evalFixed(rule as FixedRule, ctx);
|
|
97
|
+
case 'UNIT_RATE': return this.evalUnitRate(rule as UnitRateRule, ctx);
|
|
98
|
+
case 'TIERED': return this.evalTiered(rule as TieredRule, ctx);
|
|
99
|
+
case 'PERCENTAGE': return this.evalPercentage(rule as PercentageRule, ctx);
|
|
100
|
+
case 'THRESHOLD': return this.evalThreshold(rule as ThresholdRule, ctx);
|
|
101
|
+
case 'TIME_WINDOW': return this.evalTimeWindow(rule as TimeWindowRule, ctx);
|
|
102
|
+
case 'CONDITIONAL': return this.evalConditional(rule as ConditionalRule, ctx);
|
|
103
|
+
case 'COMPOSITE': return this.evalComposite(rule as CompositeRule, ctx);
|
|
104
|
+
default:
|
|
105
|
+
throw new UMPError(`Unknown pricing primitive: ${(rule as PricingRule).primitive}`, 'INVALID_RULE');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* FIXED: flat amount per event or period.
|
|
111
|
+
*/
|
|
112
|
+
private evalFixed(rule: FixedRule, _ctx: RatingContext): number {
|
|
113
|
+
return rule.amount;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* UNIT_RATE: rate × quantity.
|
|
118
|
+
*/
|
|
119
|
+
private evalUnitRate(rule: UnitRateRule, ctx: RatingContext): number {
|
|
120
|
+
return rule.rate * ctx.event.quantity;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* TIERED: different rates at different consumption levels.
|
|
125
|
+
* Supports GRADUATED (each unit at its tier's rate) and VOLUME (all units at qualifying tier).
|
|
126
|
+
*/
|
|
127
|
+
private evalTiered(rule: TieredRule, ctx: RatingContext): number {
|
|
128
|
+
const quantity = ctx.cumulativeQuantity ?? ctx.event.quantity;
|
|
129
|
+
|
|
130
|
+
if (rule.mode === 'VOLUME') {
|
|
131
|
+
// All units priced at the tier the total quantity falls into
|
|
132
|
+
const tier = rule.tiers.find(t =>
|
|
133
|
+
quantity >= t.from && (t.to === null || quantity <= t.to)
|
|
134
|
+
);
|
|
135
|
+
return (tier?.rate ?? rule.tiers[rule.tiers.length - 1].rate) * ctx.event.quantity;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// GRADUATED: each unit priced at its tier's rate
|
|
139
|
+
let total = 0;
|
|
140
|
+
let remaining = ctx.event.quantity;
|
|
141
|
+
// Calculate the starting position based on cumulative minus current event
|
|
142
|
+
let position = (ctx.cumulativeQuantity ?? ctx.event.quantity) - ctx.event.quantity;
|
|
143
|
+
|
|
144
|
+
for (const tier of rule.tiers) {
|
|
145
|
+
if (remaining <= 0) break;
|
|
146
|
+
const tierEnd = tier.to ?? Infinity;
|
|
147
|
+
const tierStart = tier.from;
|
|
148
|
+
|
|
149
|
+
// How much of this event falls in this tier?
|
|
150
|
+
if (position >= tierEnd) continue; // already past this tier
|
|
151
|
+
|
|
152
|
+
const effectiveStart = Math.max(position, tierStart);
|
|
153
|
+
const effectiveEnd = Math.min(position + remaining, tierEnd);
|
|
154
|
+
const unitsInTier = Math.max(0, effectiveEnd - effectiveStart);
|
|
155
|
+
|
|
156
|
+
total += unitsInTier * tier.rate;
|
|
157
|
+
remaining -= unitsInTier;
|
|
158
|
+
position += unitsInTier;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return total;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* PERCENTAGE: fraction of a reference amount.
|
|
166
|
+
*/
|
|
167
|
+
private evalPercentage(rule: PercentageRule, ctx: RatingContext): number {
|
|
168
|
+
const reference = ctx.referenceAmount ??
|
|
169
|
+
(ctx.event.dimensions[rule.referenceField] as number) ?? 0;
|
|
170
|
+
|
|
171
|
+
let amount = reference * rule.percentage;
|
|
172
|
+
if (rule.min !== undefined) amount = Math.max(amount, rule.min);
|
|
173
|
+
if (rule.max !== undefined) amount = Math.min(amount, rule.max);
|
|
174
|
+
|
|
175
|
+
return amount;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* THRESHOLD: binary trigger — different rates above/below threshold.
|
|
180
|
+
*/
|
|
181
|
+
private evalThreshold(rule: ThresholdRule, ctx: RatingContext): number {
|
|
182
|
+
const quantity = ctx.cumulativeQuantity ?? ctx.event.quantity;
|
|
183
|
+
if (quantity <= rule.threshold) {
|
|
184
|
+
return ctx.event.quantity * rule.belowRate;
|
|
185
|
+
}
|
|
186
|
+
return ctx.event.quantity * rule.aboveRate;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* TIME_WINDOW: rate varies by when consumption occurs.
|
|
191
|
+
*/
|
|
192
|
+
private evalTimeWindow(rule: TimeWindowRule, ctx: RatingContext): number {
|
|
193
|
+
const time = ctx.currentTime ?? ctx.event.timestamp;
|
|
194
|
+
const hour = time.getHours(); // simplified; production would use timezone
|
|
195
|
+
const day = time.getDay();
|
|
196
|
+
|
|
197
|
+
for (const window of rule.windows) {
|
|
198
|
+
const dayMatch = !window.dayOfWeek || window.dayOfWeek.includes(day);
|
|
199
|
+
const hourMatch = hour >= window.startHour && hour < window.endHour;
|
|
200
|
+
if (dayMatch && hourMatch) {
|
|
201
|
+
return ctx.event.quantity * window.rate;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return ctx.event.quantity * rule.defaultRate;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* CONDITIONAL: price depends on an attribute or outcome.
|
|
210
|
+
*/
|
|
211
|
+
private evalConditional(rule: ConditionalRule, ctx: RatingContext): number {
|
|
212
|
+
const fieldValue = ctx.event.dimensions[rule.field] ??
|
|
213
|
+
ctx.attributes?.[rule.field];
|
|
214
|
+
|
|
215
|
+
for (const branch of rule.branches) {
|
|
216
|
+
if (this.evaluateCondition(branch.condition, fieldValue)) {
|
|
217
|
+
return this.evaluate(branch.rule, ctx);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (rule.fallback) {
|
|
222
|
+
return this.evaluate(rule.fallback, ctx);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* COMPOSITE: combines multiple rules with an operator.
|
|
230
|
+
*/
|
|
231
|
+
private evalComposite(rule: CompositeRule, ctx: RatingContext): number {
|
|
232
|
+
const amounts = rule.rules.map(r => this.evaluate(r, ctx));
|
|
233
|
+
|
|
234
|
+
switch (rule.operator) {
|
|
235
|
+
case 'ADD':
|
|
236
|
+
return amounts.reduce((sum, a) => sum + a, 0);
|
|
237
|
+
case 'MAX':
|
|
238
|
+
return Math.max(...amounts);
|
|
239
|
+
case 'MIN':
|
|
240
|
+
return Math.min(...amounts);
|
|
241
|
+
case 'FIRST_MATCH':
|
|
242
|
+
// Return first non-zero amount
|
|
243
|
+
return amounts.find(a => a > 0) ?? 0;
|
|
244
|
+
default:
|
|
245
|
+
throw new UMPError(`Unknown composite operator: ${rule.operator}`, 'INVALID_RULE');
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── Condition evaluator (simplified expression engine) ──
|
|
250
|
+
|
|
251
|
+
private evaluateCondition(condition: string, value: unknown): boolean {
|
|
252
|
+
// Simple equality/comparison: "outcome == 'SUCCESS'"
|
|
253
|
+
const eqMatch = condition.match(/^(\w+)\s*==\s*'([^']+)'$/);
|
|
254
|
+
if (eqMatch) return String(value) === eqMatch[2];
|
|
255
|
+
|
|
256
|
+
const neqMatch = condition.match(/^(\w+)\s*!=\s*'([^']+)'$/);
|
|
257
|
+
if (neqMatch) return String(value) !== neqMatch[2];
|
|
258
|
+
|
|
259
|
+
const gtMatch = condition.match(/^(\w+)\s*>\s*(\d+(?:\.\d+)?)$/);
|
|
260
|
+
if (gtMatch) return Number(value) > Number(gtMatch[2]);
|
|
261
|
+
|
|
262
|
+
const ltMatch = condition.match(/^(\w+)\s*<\s*(\d+(?:\.\d+)?)$/);
|
|
263
|
+
if (ltMatch) return Number(value) < Number(ltMatch[2]);
|
|
264
|
+
|
|
265
|
+
const gteMatch = condition.match(/^(\w+)\s*>=\s*(\d+(?:\.\d+)?)$/);
|
|
266
|
+
if (gteMatch) return Number(value) >= Number(gteMatch[2]);
|
|
267
|
+
|
|
268
|
+
// Simple truthy check
|
|
269
|
+
if (condition === String(value)) return true;
|
|
270
|
+
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── Explanation builder ──
|
|
275
|
+
|
|
276
|
+
private buildExplanation(rule: PricingRule, ctx: RatingContext, depth: number): string {
|
|
277
|
+
const indent = ' '.repeat(depth);
|
|
278
|
+
const amount = round(this.evaluate(rule, ctx));
|
|
279
|
+
|
|
280
|
+
switch (rule.primitive) {
|
|
281
|
+
case 'FIXED':
|
|
282
|
+
return `${indent}FIXED: $${(rule as FixedRule).amount} flat = $${amount}`;
|
|
283
|
+
case 'UNIT_RATE':
|
|
284
|
+
return `${indent}UNIT_RATE: ${ctx.event.quantity} × $${(rule as UnitRateRule).rate}/${(rule as UnitRateRule).unit} = $${amount}`;
|
|
285
|
+
case 'TIERED':
|
|
286
|
+
return `${indent}TIERED (${(rule as TieredRule).mode}): ${ctx.event.quantity} units across ${(rule as TieredRule).tiers.length} tiers = $${amount}`;
|
|
287
|
+
case 'PERCENTAGE':
|
|
288
|
+
return `${indent}PERCENTAGE: ${((rule as PercentageRule).percentage * 100).toFixed(1)}% of reference = $${amount}`;
|
|
289
|
+
case 'THRESHOLD':
|
|
290
|
+
return `${indent}THRESHOLD: ${ctx.event.quantity > (rule as ThresholdRule).threshold ? 'above' : 'below'} ${(rule as ThresholdRule).threshold} → $${amount}`;
|
|
291
|
+
case 'TIME_WINDOW':
|
|
292
|
+
return `${indent}TIME_WINDOW: rate at ${ctx.event.timestamp.toISOString()} = $${amount}`;
|
|
293
|
+
case 'CONDITIONAL': {
|
|
294
|
+
const lines = [`${indent}CONDITIONAL on "${(rule as ConditionalRule).field}":`];
|
|
295
|
+
lines.push(`${indent} → result = $${amount}`);
|
|
296
|
+
return lines.join('\n');
|
|
297
|
+
}
|
|
298
|
+
case 'COMPOSITE': {
|
|
299
|
+
const comp = rule as CompositeRule;
|
|
300
|
+
const lines = [`${indent}COMPOSITE (${comp.operator}):`];
|
|
301
|
+
for (const sub of comp.rules) {
|
|
302
|
+
lines.push(this.buildExplanation(sub, ctx, depth + 1));
|
|
303
|
+
}
|
|
304
|
+
lines.push(`${indent} = $${amount}`);
|
|
305
|
+
return lines.join('\n');
|
|
306
|
+
}
|
|
307
|
+
default:
|
|
308
|
+
return `${indent}UNKNOWN: $${amount}`;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import type { PricingRule } from '../types';
|
|
2
|
+
import { generateId } from '../utils/id';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pre-built pricing rule templates for common AI-era models.
|
|
6
|
+
* These are convenience factories — all decompose into the 8 atomic primitives.
|
|
7
|
+
*/
|
|
8
|
+
export const PricingTemplates = {
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Per-Token LLM pricing (separate input/output rates).
|
|
12
|
+
* Example: GPT-4 at $30/M input, $60/M output tokens.
|
|
13
|
+
*/
|
|
14
|
+
perToken(inputRatePerMillion: number, outputRatePerMillion: number): PricingRule {
|
|
15
|
+
return {
|
|
16
|
+
ruleId: generateId('rul'),
|
|
17
|
+
name: 'Per-Token LLM',
|
|
18
|
+
description: `$${inputRatePerMillion}/M input, $${outputRatePerMillion}/M output tokens`,
|
|
19
|
+
primitive: 'CONDITIONAL',
|
|
20
|
+
field: 'direction',
|
|
21
|
+
branches: [
|
|
22
|
+
{
|
|
23
|
+
condition: "direction == 'input'",
|
|
24
|
+
rule: {
|
|
25
|
+
ruleId: generateId('rul'),
|
|
26
|
+
name: 'Input Token Rate',
|
|
27
|
+
primitive: 'UNIT_RATE',
|
|
28
|
+
rate: inputRatePerMillion / 1_000_000,
|
|
29
|
+
unit: 'TOKEN',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
condition: "direction == 'output'",
|
|
34
|
+
rule: {
|
|
35
|
+
ruleId: generateId('rul'),
|
|
36
|
+
name: 'Output Token Rate',
|
|
37
|
+
primitive: 'UNIT_RATE',
|
|
38
|
+
rate: outputRatePerMillion / 1_000_000,
|
|
39
|
+
unit: 'TOKEN',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Per-Inference pricing with volume tiers.
|
|
48
|
+
* Example: $0.01/call for first 10K, $0.005/call after.
|
|
49
|
+
*/
|
|
50
|
+
perInference(tiers: Array<{ upTo: number | null; rate: number }>): PricingRule {
|
|
51
|
+
return {
|
|
52
|
+
ruleId: generateId('rul'),
|
|
53
|
+
name: 'Per-Inference Tiered',
|
|
54
|
+
primitive: 'TIERED',
|
|
55
|
+
mode: 'GRADUATED',
|
|
56
|
+
tiers: tiers.map((t, i) => ({
|
|
57
|
+
from: i === 0 ? 0 : (tiers[i - 1].upTo ?? 0),
|
|
58
|
+
to: t.upTo,
|
|
59
|
+
rate: t.rate,
|
|
60
|
+
})),
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Per-Resolution / outcome-based pricing.
|
|
66
|
+
* Charges only on successful outcome + percentage of value.
|
|
67
|
+
*/
|
|
68
|
+
perResolution(successFee: number, valuePct: number): PricingRule {
|
|
69
|
+
return {
|
|
70
|
+
ruleId: generateId('rul'),
|
|
71
|
+
name: 'Per-Resolution Outcome',
|
|
72
|
+
primitive: 'CONDITIONAL',
|
|
73
|
+
field: 'outcome',
|
|
74
|
+
branches: [
|
|
75
|
+
{
|
|
76
|
+
condition: "outcome == 'SUCCESS'",
|
|
77
|
+
rule: {
|
|
78
|
+
ruleId: generateId('rul'),
|
|
79
|
+
name: 'Success Composite',
|
|
80
|
+
primitive: 'COMPOSITE',
|
|
81
|
+
operator: 'ADD',
|
|
82
|
+
rules: [
|
|
83
|
+
{
|
|
84
|
+
ruleId: generateId('rul'),
|
|
85
|
+
name: 'Success Fee',
|
|
86
|
+
primitive: 'FIXED',
|
|
87
|
+
amount: successFee,
|
|
88
|
+
period: 'PER_EVENT',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
ruleId: generateId('rul'),
|
|
92
|
+
name: 'Value Share',
|
|
93
|
+
primitive: 'PERCENTAGE',
|
|
94
|
+
percentage: valuePct,
|
|
95
|
+
referenceField: 'value_generated',
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
fallback: {
|
|
102
|
+
ruleId: generateId('rul'),
|
|
103
|
+
name: 'No charge on failure',
|
|
104
|
+
primitive: 'FIXED',
|
|
105
|
+
amount: 0,
|
|
106
|
+
period: 'PER_EVENT',
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Subscription + Usage hybrid.
|
|
113
|
+
* Base monthly fee + per-unit overage above included units.
|
|
114
|
+
*/
|
|
115
|
+
subscriptionPlusUsage(
|
|
116
|
+
monthlyBase: number,
|
|
117
|
+
includedUnits: number,
|
|
118
|
+
overageRate: number
|
|
119
|
+
): PricingRule {
|
|
120
|
+
return {
|
|
121
|
+
ruleId: generateId('rul'),
|
|
122
|
+
name: 'Subscription + Usage',
|
|
123
|
+
primitive: 'COMPOSITE',
|
|
124
|
+
operator: 'ADD',
|
|
125
|
+
rules: [
|
|
126
|
+
{
|
|
127
|
+
ruleId: generateId('rul'),
|
|
128
|
+
name: 'Monthly Base',
|
|
129
|
+
primitive: 'FIXED',
|
|
130
|
+
amount: monthlyBase,
|
|
131
|
+
period: 'MONTHLY',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
ruleId: generateId('rul'),
|
|
135
|
+
name: 'Overage',
|
|
136
|
+
primitive: 'THRESHOLD',
|
|
137
|
+
threshold: includedUnits,
|
|
138
|
+
belowRate: 0,
|
|
139
|
+
aboveRate: overageRate,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Credit pool with per-unit drawdown.
|
|
147
|
+
*/
|
|
148
|
+
creditPool(creditCost: number): PricingRule {
|
|
149
|
+
return {
|
|
150
|
+
ruleId: generateId('rul'),
|
|
151
|
+
name: 'Credit Pool',
|
|
152
|
+
primitive: 'UNIT_RATE',
|
|
153
|
+
rate: creditCost,
|
|
154
|
+
unit: 'CREDIT',
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Spot compute pricing with peak/off-peak rates.
|
|
160
|
+
*/
|
|
161
|
+
computeSpot(
|
|
162
|
+
peakRate: number,
|
|
163
|
+
offPeakRate: number,
|
|
164
|
+
peakHours: [number, number] = [9, 17]
|
|
165
|
+
): PricingRule {
|
|
166
|
+
return {
|
|
167
|
+
ruleId: generateId('rul'),
|
|
168
|
+
name: 'Compute Spot',
|
|
169
|
+
primitive: 'TIME_WINDOW',
|
|
170
|
+
windows: [
|
|
171
|
+
{
|
|
172
|
+
startHour: peakHours[0],
|
|
173
|
+
endHour: peakHours[1],
|
|
174
|
+
rate: peakRate,
|
|
175
|
+
label: 'peak',
|
|
176
|
+
dayOfWeek: [1, 2, 3, 4, 5], // weekdays
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
defaultRate: offPeakRate,
|
|
180
|
+
timezone: 'UTC',
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Marketplace commission (percentage take rate with floor and cap).
|
|
186
|
+
*/
|
|
187
|
+
marketplaceCommission(takeRate: number, minFee: number, maxFee: number): PricingRule {
|
|
188
|
+
return {
|
|
189
|
+
ruleId: generateId('rul'),
|
|
190
|
+
name: 'Marketplace Commission',
|
|
191
|
+
primitive: 'PERCENTAGE',
|
|
192
|
+
percentage: takeRate,
|
|
193
|
+
referenceField: 'transaction_amount',
|
|
194
|
+
min: minFee,
|
|
195
|
+
max: maxFee,
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Agent-to-agent task pricing (fixed per-task + outcome bonus).
|
|
201
|
+
*/
|
|
202
|
+
agentTask(basePerTask: number, bonusOnSuccess: number): PricingRule {
|
|
203
|
+
return {
|
|
204
|
+
ruleId: generateId('rul'),
|
|
205
|
+
name: 'Agent Task',
|
|
206
|
+
primitive: 'COMPOSITE',
|
|
207
|
+
operator: 'ADD',
|
|
208
|
+
rules: [
|
|
209
|
+
{
|
|
210
|
+
ruleId: generateId('rul'),
|
|
211
|
+
name: 'Base Task Fee',
|
|
212
|
+
primitive: 'FIXED',
|
|
213
|
+
amount: basePerTask,
|
|
214
|
+
period: 'PER_EVENT',
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
ruleId: generateId('rul'),
|
|
218
|
+
name: 'Success Bonus',
|
|
219
|
+
primitive: 'CONDITIONAL',
|
|
220
|
+
field: 'outcome',
|
|
221
|
+
branches: [
|
|
222
|
+
{
|
|
223
|
+
condition: "outcome == 'SUCCESS'",
|
|
224
|
+
rule: {
|
|
225
|
+
ruleId: generateId('rul'),
|
|
226
|
+
name: 'Bonus',
|
|
227
|
+
primitive: 'FIXED',
|
|
228
|
+
amount: bonusOnSuccess,
|
|
229
|
+
period: 'PER_EVENT',
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
};
|
|
236
|
+
},
|
|
237
|
+
};
|