@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,1520 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
AgentManager: () => AgentManager,
|
|
34
|
+
AgentNotFoundError: () => AgentNotFoundError,
|
|
35
|
+
AgentRevokedError: () => AgentRevokedError,
|
|
36
|
+
AuditTrail: () => AuditTrail,
|
|
37
|
+
AuthorityExceededError: () => AuthorityExceededError,
|
|
38
|
+
ContractManager: () => ContractManager,
|
|
39
|
+
ContractNotFoundError: () => ContractNotFoundError,
|
|
40
|
+
DisputeError: () => DisputeError,
|
|
41
|
+
InsufficientFundsError: () => InsufficientFundsError,
|
|
42
|
+
MeteringEngine: () => MeteringEngine,
|
|
43
|
+
PolicyViolationError: () => PolicyViolationError,
|
|
44
|
+
PricingEngine: () => PricingEngine,
|
|
45
|
+
PricingTemplates: () => PricingTemplates,
|
|
46
|
+
SettlementBus: () => SettlementBus,
|
|
47
|
+
UMP: () => UMP,
|
|
48
|
+
UMPError: () => UMPError,
|
|
49
|
+
WalletFrozenError: () => WalletFrozenError,
|
|
50
|
+
WalletManager: () => WalletManager
|
|
51
|
+
});
|
|
52
|
+
module.exports = __toCommonJS(index_exports);
|
|
53
|
+
|
|
54
|
+
// src/utils/id.ts
|
|
55
|
+
var import_uuid = require("uuid");
|
|
56
|
+
function generateId(prefix) {
|
|
57
|
+
return `${prefix}_${(0, import_uuid.v4)()}`;
|
|
58
|
+
}
|
|
59
|
+
function parseMoney(value) {
|
|
60
|
+
if (typeof value === "number") return value;
|
|
61
|
+
const cleaned = value.replace(/[^0-9.-]/g, "");
|
|
62
|
+
const parsed = parseFloat(cleaned);
|
|
63
|
+
if (isNaN(parsed)) throw new Error(`Invalid monetary value: ${value}`);
|
|
64
|
+
return parsed;
|
|
65
|
+
}
|
|
66
|
+
function round(value, decimals = 2) {
|
|
67
|
+
const factor = Math.pow(10, decimals);
|
|
68
|
+
return Math.round(value * factor) / factor;
|
|
69
|
+
}
|
|
70
|
+
function hrTimestamp() {
|
|
71
|
+
return /* @__PURE__ */ new Date();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/utils/errors.ts
|
|
75
|
+
var UMPError = class extends Error {
|
|
76
|
+
code;
|
|
77
|
+
constructor(message, code) {
|
|
78
|
+
super(message);
|
|
79
|
+
this.name = "UMPError";
|
|
80
|
+
this.code = code;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var AuthorityExceededError = class extends UMPError {
|
|
84
|
+
limit;
|
|
85
|
+
attempted;
|
|
86
|
+
constructor(limitType, limit, attempted) {
|
|
87
|
+
super(
|
|
88
|
+
`Authority exceeded: ${limitType} limit is ${limit}, attempted ${attempted}`,
|
|
89
|
+
"AUTHORITY_EXCEEDED"
|
|
90
|
+
);
|
|
91
|
+
this.name = "AuthorityExceededError";
|
|
92
|
+
this.limit = limit;
|
|
93
|
+
this.attempted = attempted;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
var InsufficientFundsError = class extends UMPError {
|
|
97
|
+
available;
|
|
98
|
+
required;
|
|
99
|
+
constructor(available, required) {
|
|
100
|
+
super(
|
|
101
|
+
`Insufficient funds: available ${available}, required ${required}`,
|
|
102
|
+
"INSUFFICIENT_FUNDS"
|
|
103
|
+
);
|
|
104
|
+
this.name = "InsufficientFundsError";
|
|
105
|
+
this.available = available;
|
|
106
|
+
this.required = required;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
var WalletFrozenError = class extends UMPError {
|
|
110
|
+
constructor(walletId) {
|
|
111
|
+
super(`Wallet ${walletId} is frozen \u2014 all transactions blocked`, "WALLET_FROZEN");
|
|
112
|
+
this.name = "WalletFrozenError";
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var AgentNotFoundError = class extends UMPError {
|
|
116
|
+
constructor(agentId) {
|
|
117
|
+
super(`Agent not found: ${agentId}`, "AGENT_NOT_FOUND");
|
|
118
|
+
this.name = "AgentNotFoundError";
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
var AgentRevokedError = class extends UMPError {
|
|
122
|
+
constructor(agentId) {
|
|
123
|
+
super(`Agent ${agentId} has been revoked`, "AGENT_REVOKED");
|
|
124
|
+
this.name = "AgentRevokedError";
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
var ContractNotFoundError = class extends UMPError {
|
|
128
|
+
constructor(contractId) {
|
|
129
|
+
super(`Contract not found: ${contractId}`, "CONTRACT_NOT_FOUND");
|
|
130
|
+
this.name = "ContractNotFoundError";
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
var PolicyViolationError = class extends UMPError {
|
|
134
|
+
policyType;
|
|
135
|
+
action;
|
|
136
|
+
constructor(policyType, action, details) {
|
|
137
|
+
super(`Policy violation [${policyType}]: ${details}`, "POLICY_VIOLATION");
|
|
138
|
+
this.name = "PolicyViolationError";
|
|
139
|
+
this.policyType = policyType;
|
|
140
|
+
this.action = action;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
var DisputeError = class extends UMPError {
|
|
144
|
+
constructor(message) {
|
|
145
|
+
super(message, "DISPUTE_ERROR");
|
|
146
|
+
this.name = "DisputeError";
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// src/core/agent-manager.ts
|
|
151
|
+
var import_crypto = __toESM(require("crypto"));
|
|
152
|
+
var AgentManager = class {
|
|
153
|
+
agents = /* @__PURE__ */ new Map();
|
|
154
|
+
/**
|
|
155
|
+
* Register a new Agent Identity with authority scope.
|
|
156
|
+
*/
|
|
157
|
+
create(options) {
|
|
158
|
+
const agentId = generateId("agt");
|
|
159
|
+
const now = hrTimestamp();
|
|
160
|
+
const { publicKey, privateKey } = import_crypto.default.generateKeyPairSync("ed25519");
|
|
161
|
+
const pubKeyHex = publicKey.export({ type: "spki", format: "der" }).toString("hex");
|
|
162
|
+
const authority = {
|
|
163
|
+
maxPerTransaction: parseMoney(options.authority.maxPerTransaction),
|
|
164
|
+
maxPerDay: parseMoney(options.authority.maxPerDay),
|
|
165
|
+
maxPerMonth: options.authority.maxPerMonth ? parseMoney(options.authority.maxPerMonth) : parseMoney(options.authority.maxPerDay) * 30,
|
|
166
|
+
allowedServices: options.authority.allowedServices,
|
|
167
|
+
allowedCounterparties: options.authority.allowedCounterparties,
|
|
168
|
+
requiresApprovalAbove: options.authority.requiresApprovalAbove,
|
|
169
|
+
autoRevokeAfter: options.authority.autoRevokeAfter
|
|
170
|
+
};
|
|
171
|
+
if (options.parentId) {
|
|
172
|
+
const parent = this.get(options.parentId);
|
|
173
|
+
authority.maxPerTransaction = Math.min(authority.maxPerTransaction, parent.authorityScope.maxPerTransaction);
|
|
174
|
+
authority.maxPerDay = Math.min(authority.maxPerDay, parent.authorityScope.maxPerDay);
|
|
175
|
+
authority.maxPerMonth = Math.min(authority.maxPerMonth, parent.authorityScope.maxPerMonth);
|
|
176
|
+
}
|
|
177
|
+
const verification = {
|
|
178
|
+
publicKey: pubKeyHex,
|
|
179
|
+
issuingAuthority: "ump-sdk-local",
|
|
180
|
+
expiresAt: new Date(now.getTime() + (options.authority.autoRevokeAfter || 365 * 24 * 60 * 60 * 1e3))
|
|
181
|
+
};
|
|
182
|
+
const agent = {
|
|
183
|
+
agentId,
|
|
184
|
+
agentType: options.type,
|
|
185
|
+
parentId: options.parentId || null,
|
|
186
|
+
displayName: options.name,
|
|
187
|
+
capabilities: options.capabilities || [],
|
|
188
|
+
authorityScope: authority,
|
|
189
|
+
verification,
|
|
190
|
+
metadata: {
|
|
191
|
+
...options.metadata,
|
|
192
|
+
_privateKey: privateKey.export({ type: "pkcs8", format: "der" }).toString("hex")
|
|
193
|
+
},
|
|
194
|
+
status: "ACTIVE",
|
|
195
|
+
createdAt: now,
|
|
196
|
+
updatedAt: now
|
|
197
|
+
};
|
|
198
|
+
this.agents.set(agentId, agent);
|
|
199
|
+
if (options.authority.autoRevokeAfter) {
|
|
200
|
+
setTimeout(() => this.revoke(agentId), options.authority.autoRevokeAfter);
|
|
201
|
+
}
|
|
202
|
+
return agent;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Retrieve an agent by ID.
|
|
206
|
+
*/
|
|
207
|
+
get(agentId) {
|
|
208
|
+
const agent = this.agents.get(agentId);
|
|
209
|
+
if (!agent) throw new AgentNotFoundError(agentId);
|
|
210
|
+
return agent;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Update authority scope (tightening is always allowed; loosening requires parent).
|
|
214
|
+
*/
|
|
215
|
+
updateAuthority(agentId, newScope) {
|
|
216
|
+
const agent = this.get(agentId);
|
|
217
|
+
if (agent.status !== "ACTIVE") throw new AgentRevokedError(agentId);
|
|
218
|
+
const updated = {
|
|
219
|
+
...agent,
|
|
220
|
+
authorityScope: { ...agent.authorityScope, ...newScope },
|
|
221
|
+
updatedAt: hrTimestamp()
|
|
222
|
+
};
|
|
223
|
+
this.agents.set(agentId, updated);
|
|
224
|
+
return updated;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Revoke an agent and cascade to all children.
|
|
228
|
+
*/
|
|
229
|
+
revoke(agentId) {
|
|
230
|
+
const revoked = [];
|
|
231
|
+
const agent = this.agents.get(agentId);
|
|
232
|
+
if (!agent) return revoked;
|
|
233
|
+
agent.status = "REVOKED";
|
|
234
|
+
agent.updatedAt = hrTimestamp();
|
|
235
|
+
revoked.push(agentId);
|
|
236
|
+
for (const [id, a] of this.agents) {
|
|
237
|
+
if (a.parentId === agentId && a.status === "ACTIVE") {
|
|
238
|
+
revoked.push(...this.revoke(id));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return revoked;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Verify that an agent's identity is valid and active.
|
|
245
|
+
*/
|
|
246
|
+
verify(agentId) {
|
|
247
|
+
const agent = this.agents.get(agentId);
|
|
248
|
+
if (!agent) return { valid: false, reason: "Agent not found" };
|
|
249
|
+
if (agent.status !== "ACTIVE") return { valid: false, reason: `Agent status: ${agent.status}` };
|
|
250
|
+
if (agent.verification.expiresAt < /* @__PURE__ */ new Date()) {
|
|
251
|
+
agent.status = "EXPIRED";
|
|
252
|
+
return { valid: false, reason: "Verification expired" };
|
|
253
|
+
}
|
|
254
|
+
return { valid: true };
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Check if a transaction amount is within the agent's authority scope.
|
|
258
|
+
*/
|
|
259
|
+
checkAuthority(agentId, amount, counterpartyId, serviceId) {
|
|
260
|
+
const agent = this.get(agentId);
|
|
261
|
+
const scope = agent.authorityScope;
|
|
262
|
+
if (agent.status !== "ACTIVE") {
|
|
263
|
+
return { allowed: false, reason: `Agent status: ${agent.status}` };
|
|
264
|
+
}
|
|
265
|
+
if (amount > scope.maxPerTransaction) {
|
|
266
|
+
return { allowed: false, reason: `Exceeds per-transaction limit of ${scope.maxPerTransaction}` };
|
|
267
|
+
}
|
|
268
|
+
if (scope.allowedCounterparties && counterpartyId) {
|
|
269
|
+
const allowed = scope.allowedCounterparties.some((pattern) => {
|
|
270
|
+
if (pattern.endsWith("*")) {
|
|
271
|
+
return counterpartyId.startsWith(pattern.slice(0, -1));
|
|
272
|
+
}
|
|
273
|
+
return counterpartyId === pattern;
|
|
274
|
+
});
|
|
275
|
+
if (!allowed) {
|
|
276
|
+
return { allowed: false, reason: `Counterparty ${counterpartyId} not in allowlist` };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (scope.allowedServices && serviceId) {
|
|
280
|
+
if (!scope.allowedServices.includes(serviceId)) {
|
|
281
|
+
return { allowed: false, reason: `Service ${serviceId} not in allowlist` };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (scope.requiresApprovalAbove && amount > scope.requiresApprovalAbove) {
|
|
285
|
+
return { allowed: true, requiresApproval: true };
|
|
286
|
+
}
|
|
287
|
+
return { allowed: true };
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* List all agents, optionally filtered.
|
|
291
|
+
*/
|
|
292
|
+
list(filter) {
|
|
293
|
+
let results = Array.from(this.agents.values());
|
|
294
|
+
if (filter?.parentId) results = results.filter((a) => a.parentId === filter.parentId);
|
|
295
|
+
if (filter?.type) results = results.filter((a) => a.agentType === filter.type);
|
|
296
|
+
if (filter?.status) results = results.filter((a) => a.status === filter.status);
|
|
297
|
+
return results;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
// src/core/wallet-manager.ts
|
|
302
|
+
var WalletManager = class {
|
|
303
|
+
wallets = /* @__PURE__ */ new Map();
|
|
304
|
+
ledgers = /* @__PURE__ */ new Map();
|
|
305
|
+
// walletId -> entries
|
|
306
|
+
/**
|
|
307
|
+
* Create a wallet for an agent.
|
|
308
|
+
*/
|
|
309
|
+
create(ownerAgentId) {
|
|
310
|
+
const walletId = generateId("wal");
|
|
311
|
+
const wallet = {
|
|
312
|
+
walletId,
|
|
313
|
+
ownerAgentId,
|
|
314
|
+
balances: [{
|
|
315
|
+
valueUnitType: "FIAT",
|
|
316
|
+
currency: "USD",
|
|
317
|
+
amount: 0,
|
|
318
|
+
reserved: 0,
|
|
319
|
+
available: 0
|
|
320
|
+
}],
|
|
321
|
+
frozen: false,
|
|
322
|
+
createdAt: hrTimestamp()
|
|
323
|
+
};
|
|
324
|
+
this.wallets.set(walletId, wallet);
|
|
325
|
+
this.ledgers.set(walletId, []);
|
|
326
|
+
return wallet;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Get wallet by ID.
|
|
330
|
+
*/
|
|
331
|
+
get(walletId) {
|
|
332
|
+
const wallet = this.wallets.get(walletId);
|
|
333
|
+
if (!wallet) throw new UMPError(`Wallet not found: ${walletId}`, "WALLET_NOT_FOUND");
|
|
334
|
+
return wallet;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Get wallet by owner agent ID.
|
|
338
|
+
*/
|
|
339
|
+
getByAgent(agentId) {
|
|
340
|
+
for (const w of this.wallets.values()) {
|
|
341
|
+
if (w.ownerAgentId === agentId) return w;
|
|
342
|
+
}
|
|
343
|
+
throw new UMPError(`No wallet found for agent: ${agentId}`, "WALLET_NOT_FOUND");
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Fund a wallet (add money).
|
|
347
|
+
*/
|
|
348
|
+
fund(walletId, options) {
|
|
349
|
+
const wallet = this.get(walletId);
|
|
350
|
+
if (wallet.frozen) throw new WalletFrozenError(walletId);
|
|
351
|
+
const amount = parseMoney(options.amount);
|
|
352
|
+
const vut = options.valueUnitType || "FIAT";
|
|
353
|
+
const currency = options.currency || "USD";
|
|
354
|
+
const balance = this.getOrCreateBalance(wallet, vut, currency);
|
|
355
|
+
balance.amount = round(balance.amount + amount);
|
|
356
|
+
balance.available = round(balance.available + amount);
|
|
357
|
+
const entry = this.appendLedger(walletId, {
|
|
358
|
+
type: "TOPUP",
|
|
359
|
+
amount,
|
|
360
|
+
valueUnitType: vut,
|
|
361
|
+
currency,
|
|
362
|
+
description: `Funded ${amount} ${currency}`,
|
|
363
|
+
balanceAfter: balance.available
|
|
364
|
+
});
|
|
365
|
+
return entry;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Reserve funds for a pending transaction (escrow pattern).
|
|
369
|
+
*/
|
|
370
|
+
reserve(walletId, amount, counterpartyAgentId, transactionId) {
|
|
371
|
+
const wallet = this.get(walletId);
|
|
372
|
+
if (wallet.frozen) throw new WalletFrozenError(walletId);
|
|
373
|
+
const balance = this.getPrimaryBalance(wallet);
|
|
374
|
+
if (balance.available < amount) {
|
|
375
|
+
throw new InsufficientFundsError(balance.available, amount);
|
|
376
|
+
}
|
|
377
|
+
balance.reserved = round(balance.reserved + amount);
|
|
378
|
+
balance.available = round(balance.available - amount);
|
|
379
|
+
return this.appendLedger(walletId, {
|
|
380
|
+
type: "RESERVE",
|
|
381
|
+
amount,
|
|
382
|
+
valueUnitType: balance.valueUnitType,
|
|
383
|
+
currency: balance.currency,
|
|
384
|
+
counterpartyAgentId,
|
|
385
|
+
transactionId,
|
|
386
|
+
description: `Reserved ${amount} for transaction ${transactionId}`,
|
|
387
|
+
balanceAfter: balance.available
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Debit wallet (immediate drawdown).
|
|
392
|
+
*/
|
|
393
|
+
debit(walletId, amount, counterpartyAgentId, transactionId) {
|
|
394
|
+
const wallet = this.get(walletId);
|
|
395
|
+
if (wallet.frozen) throw new WalletFrozenError(walletId);
|
|
396
|
+
const balance = this.getPrimaryBalance(wallet);
|
|
397
|
+
if (balance.available < amount) {
|
|
398
|
+
throw new InsufficientFundsError(balance.available, amount);
|
|
399
|
+
}
|
|
400
|
+
balance.amount = round(balance.amount - amount);
|
|
401
|
+
balance.available = round(balance.available - amount);
|
|
402
|
+
return this.appendLedger(walletId, {
|
|
403
|
+
type: "DEBIT",
|
|
404
|
+
amount,
|
|
405
|
+
valueUnitType: balance.valueUnitType,
|
|
406
|
+
currency: balance.currency,
|
|
407
|
+
counterpartyAgentId,
|
|
408
|
+
transactionId,
|
|
409
|
+
description: `Debited ${amount} to ${counterpartyAgentId}`,
|
|
410
|
+
balanceAfter: balance.available
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Credit wallet (receive payment).
|
|
415
|
+
*/
|
|
416
|
+
credit(walletId, amount, counterpartyAgentId, transactionId) {
|
|
417
|
+
const wallet = this.get(walletId);
|
|
418
|
+
const balance = this.getPrimaryBalance(wallet);
|
|
419
|
+
balance.amount = round(balance.amount + amount);
|
|
420
|
+
balance.available = round(balance.available + amount);
|
|
421
|
+
return this.appendLedger(walletId, {
|
|
422
|
+
type: "CREDIT",
|
|
423
|
+
amount,
|
|
424
|
+
valueUnitType: balance.valueUnitType,
|
|
425
|
+
currency: balance.currency,
|
|
426
|
+
counterpartyAgentId,
|
|
427
|
+
transactionId,
|
|
428
|
+
description: `Credited ${amount} from ${counterpartyAgentId}`,
|
|
429
|
+
balanceAfter: balance.available
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Release reserved funds (escrow release after outcome).
|
|
434
|
+
*/
|
|
435
|
+
releaseReservation(walletId, amount, transactionId) {
|
|
436
|
+
const wallet = this.get(walletId);
|
|
437
|
+
const balance = this.getPrimaryBalance(wallet);
|
|
438
|
+
const releaseAmount = Math.min(amount, balance.reserved);
|
|
439
|
+
balance.reserved = round(balance.reserved - releaseAmount);
|
|
440
|
+
balance.amount = round(balance.amount - releaseAmount);
|
|
441
|
+
return this.appendLedger(walletId, {
|
|
442
|
+
type: "RELEASE",
|
|
443
|
+
amount: releaseAmount,
|
|
444
|
+
valueUnitType: balance.valueUnitType,
|
|
445
|
+
currency: balance.currency,
|
|
446
|
+
transactionId,
|
|
447
|
+
description: `Released ${releaseAmount} from escrow for ${transactionId}`,
|
|
448
|
+
balanceAfter: balance.available
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Emergency freeze — blocks all transactions.
|
|
453
|
+
*/
|
|
454
|
+
freeze(walletId) {
|
|
455
|
+
const wallet = this.get(walletId);
|
|
456
|
+
wallet.frozen = true;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Unfreeze wallet.
|
|
460
|
+
*/
|
|
461
|
+
unfreeze(walletId) {
|
|
462
|
+
const wallet = this.get(walletId);
|
|
463
|
+
wallet.frozen = false;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Get immutable ledger entries for a wallet.
|
|
467
|
+
*/
|
|
468
|
+
getLedger(walletId, limit = 100, offset = 0) {
|
|
469
|
+
const entries = this.ledgers.get(walletId) || [];
|
|
470
|
+
return entries.slice(offset, offset + limit);
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Get real-time balance summary.
|
|
474
|
+
*/
|
|
475
|
+
getBalance(walletId) {
|
|
476
|
+
const wallet = this.get(walletId);
|
|
477
|
+
return wallet.balances.map((b) => ({ ...b }));
|
|
478
|
+
}
|
|
479
|
+
// ── Private helpers ──
|
|
480
|
+
getPrimaryBalance(wallet) {
|
|
481
|
+
if (wallet.balances.length === 0) {
|
|
482
|
+
const balance = {
|
|
483
|
+
valueUnitType: "FIAT",
|
|
484
|
+
currency: "USD",
|
|
485
|
+
amount: 0,
|
|
486
|
+
reserved: 0,
|
|
487
|
+
available: 0
|
|
488
|
+
};
|
|
489
|
+
wallet.balances.push(balance);
|
|
490
|
+
return balance;
|
|
491
|
+
}
|
|
492
|
+
return wallet.balances[0];
|
|
493
|
+
}
|
|
494
|
+
getOrCreateBalance(wallet, vut, currency) {
|
|
495
|
+
let balance = wallet.balances.find(
|
|
496
|
+
(b) => b.valueUnitType === vut && b.currency === currency
|
|
497
|
+
);
|
|
498
|
+
if (!balance) {
|
|
499
|
+
balance = { valueUnitType: vut, currency, amount: 0, reserved: 0, available: 0 };
|
|
500
|
+
wallet.balances.push(balance);
|
|
501
|
+
}
|
|
502
|
+
return balance;
|
|
503
|
+
}
|
|
504
|
+
appendLedger(walletId, data) {
|
|
505
|
+
const entry = {
|
|
506
|
+
entryId: generateId("led"),
|
|
507
|
+
timestamp: hrTimestamp(),
|
|
508
|
+
...data
|
|
509
|
+
};
|
|
510
|
+
const ledger = this.ledgers.get(walletId);
|
|
511
|
+
if (ledger) ledger.push(entry);
|
|
512
|
+
return entry;
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// src/core/audit-trail.ts
|
|
517
|
+
var AuditTrail = class {
|
|
518
|
+
records = [];
|
|
519
|
+
onAudit;
|
|
520
|
+
constructor(onAudit) {
|
|
521
|
+
this.onAudit = onAudit;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Record an audit entry. Returns the audit ID.
|
|
525
|
+
*/
|
|
526
|
+
record(data) {
|
|
527
|
+
const auditId = generateId("aud");
|
|
528
|
+
const record = {
|
|
529
|
+
auditId,
|
|
530
|
+
when: hrTimestamp(),
|
|
531
|
+
...data
|
|
532
|
+
};
|
|
533
|
+
this.records.push(record);
|
|
534
|
+
if (this.onAudit) {
|
|
535
|
+
this.onAudit(record);
|
|
536
|
+
}
|
|
537
|
+
return auditId;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Query audit records with filters.
|
|
541
|
+
*/
|
|
542
|
+
query(filters, limit = 100, offset = 0) {
|
|
543
|
+
let results = this.records;
|
|
544
|
+
if (filters) {
|
|
545
|
+
if (filters.agentId) {
|
|
546
|
+
results = results.filter(
|
|
547
|
+
(r) => r.who.sourceAgentId === filters.agentId || r.who.targetAgentId === filters.agentId
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
if (filters.operation) {
|
|
551
|
+
results = results.filter((r) => r.what.operation === filters.operation);
|
|
552
|
+
}
|
|
553
|
+
if (filters.fromDate) {
|
|
554
|
+
results = results.filter((r) => r.when >= filters.fromDate);
|
|
555
|
+
}
|
|
556
|
+
if (filters.toDate) {
|
|
557
|
+
results = results.filter((r) => r.when <= filters.toDate);
|
|
558
|
+
}
|
|
559
|
+
if (filters.minAmount !== void 0) {
|
|
560
|
+
results = results.filter((r) => (r.what.amount || 0) >= filters.minAmount);
|
|
561
|
+
}
|
|
562
|
+
if (filters.contractId) {
|
|
563
|
+
results = results.filter((r) => r.why.contractId === filters.contractId);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return results.slice(offset, offset + limit);
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Get a specific audit record by ID.
|
|
570
|
+
*/
|
|
571
|
+
get(auditId) {
|
|
572
|
+
return this.records.find((r) => r.auditId === auditId);
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Get total count of records.
|
|
576
|
+
*/
|
|
577
|
+
count() {
|
|
578
|
+
return this.records.length;
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
// src/pricing/engine.ts
|
|
583
|
+
var PricingEngine = class {
|
|
584
|
+
/**
|
|
585
|
+
* Rate a single usage event against a pricing rule.
|
|
586
|
+
* Returns the calculated amount.
|
|
587
|
+
*/
|
|
588
|
+
rate(rule, context) {
|
|
589
|
+
const amount = this.evaluate(rule, context);
|
|
590
|
+
return {
|
|
591
|
+
ratedRecordId: generateId("rat"),
|
|
592
|
+
usageEventId: context.event.eventId,
|
|
593
|
+
contractId: context.event.contractId,
|
|
594
|
+
pricingRuleId: rule.ruleId,
|
|
595
|
+
quantity: context.event.quantity,
|
|
596
|
+
rate: context.event.quantity > 0 ? round(amount / context.event.quantity, 6) : 0,
|
|
597
|
+
amount: round(amount),
|
|
598
|
+
currency: "USD",
|
|
599
|
+
// default, override via contract
|
|
600
|
+
ratedAt: hrTimestamp()
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Rate multiple usage events against a pricing rule.
|
|
605
|
+
*/
|
|
606
|
+
rateBatch(rule, events) {
|
|
607
|
+
let cumulative = 0;
|
|
608
|
+
return events.map((event) => {
|
|
609
|
+
cumulative += event.quantity;
|
|
610
|
+
const context = {
|
|
611
|
+
event,
|
|
612
|
+
cumulativeQuantity: cumulative
|
|
613
|
+
};
|
|
614
|
+
return this.rate(rule, context);
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Simulate: calculate projected cost without creating rated records.
|
|
619
|
+
*/
|
|
620
|
+
simulate(rule, quantity, dimensions) {
|
|
621
|
+
const mockEvent = {
|
|
622
|
+
eventId: "sim",
|
|
623
|
+
sourceAgentId: "sim",
|
|
624
|
+
targetAgentId: "sim",
|
|
625
|
+
contractId: "sim",
|
|
626
|
+
serviceId: "sim",
|
|
627
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
628
|
+
quantity,
|
|
629
|
+
unit: "UNIT",
|
|
630
|
+
dimensions: dimensions || {}
|
|
631
|
+
};
|
|
632
|
+
return round(this.evaluate(rule, { event: mockEvent }));
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Human-readable breakdown of how a price was calculated.
|
|
636
|
+
*/
|
|
637
|
+
explain(rule, context) {
|
|
638
|
+
return this.buildExplanation(rule, context, 0);
|
|
639
|
+
}
|
|
640
|
+
// ═══════════════════════════════════════════════════════════
|
|
641
|
+
// PRIMITIVE EVALUATORS
|
|
642
|
+
// ═══════════════════════════════════════════════════════════
|
|
643
|
+
evaluate(rule, ctx) {
|
|
644
|
+
switch (rule.primitive) {
|
|
645
|
+
case "FIXED":
|
|
646
|
+
return this.evalFixed(rule, ctx);
|
|
647
|
+
case "UNIT_RATE":
|
|
648
|
+
return this.evalUnitRate(rule, ctx);
|
|
649
|
+
case "TIERED":
|
|
650
|
+
return this.evalTiered(rule, ctx);
|
|
651
|
+
case "PERCENTAGE":
|
|
652
|
+
return this.evalPercentage(rule, ctx);
|
|
653
|
+
case "THRESHOLD":
|
|
654
|
+
return this.evalThreshold(rule, ctx);
|
|
655
|
+
case "TIME_WINDOW":
|
|
656
|
+
return this.evalTimeWindow(rule, ctx);
|
|
657
|
+
case "CONDITIONAL":
|
|
658
|
+
return this.evalConditional(rule, ctx);
|
|
659
|
+
case "COMPOSITE":
|
|
660
|
+
return this.evalComposite(rule, ctx);
|
|
661
|
+
default:
|
|
662
|
+
throw new UMPError(`Unknown pricing primitive: ${rule.primitive}`, "INVALID_RULE");
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* FIXED: flat amount per event or period.
|
|
667
|
+
*/
|
|
668
|
+
evalFixed(rule, _ctx) {
|
|
669
|
+
return rule.amount;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* UNIT_RATE: rate × quantity.
|
|
673
|
+
*/
|
|
674
|
+
evalUnitRate(rule, ctx) {
|
|
675
|
+
return rule.rate * ctx.event.quantity;
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* TIERED: different rates at different consumption levels.
|
|
679
|
+
* Supports GRADUATED (each unit at its tier's rate) and VOLUME (all units at qualifying tier).
|
|
680
|
+
*/
|
|
681
|
+
evalTiered(rule, ctx) {
|
|
682
|
+
const quantity = ctx.cumulativeQuantity ?? ctx.event.quantity;
|
|
683
|
+
if (rule.mode === "VOLUME") {
|
|
684
|
+
const tier = rule.tiers.find(
|
|
685
|
+
(t) => quantity >= t.from && (t.to === null || quantity <= t.to)
|
|
686
|
+
);
|
|
687
|
+
return (tier?.rate ?? rule.tiers[rule.tiers.length - 1].rate) * ctx.event.quantity;
|
|
688
|
+
}
|
|
689
|
+
let total = 0;
|
|
690
|
+
let remaining = ctx.event.quantity;
|
|
691
|
+
let position = (ctx.cumulativeQuantity ?? ctx.event.quantity) - ctx.event.quantity;
|
|
692
|
+
for (const tier of rule.tiers) {
|
|
693
|
+
if (remaining <= 0) break;
|
|
694
|
+
const tierEnd = tier.to ?? Infinity;
|
|
695
|
+
const tierStart = tier.from;
|
|
696
|
+
if (position >= tierEnd) continue;
|
|
697
|
+
const effectiveStart = Math.max(position, tierStart);
|
|
698
|
+
const effectiveEnd = Math.min(position + remaining, tierEnd);
|
|
699
|
+
const unitsInTier = Math.max(0, effectiveEnd - effectiveStart);
|
|
700
|
+
total += unitsInTier * tier.rate;
|
|
701
|
+
remaining -= unitsInTier;
|
|
702
|
+
position += unitsInTier;
|
|
703
|
+
}
|
|
704
|
+
return total;
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* PERCENTAGE: fraction of a reference amount.
|
|
708
|
+
*/
|
|
709
|
+
evalPercentage(rule, ctx) {
|
|
710
|
+
const reference = ctx.referenceAmount ?? ctx.event.dimensions[rule.referenceField] ?? 0;
|
|
711
|
+
let amount = reference * rule.percentage;
|
|
712
|
+
if (rule.min !== void 0) amount = Math.max(amount, rule.min);
|
|
713
|
+
if (rule.max !== void 0) amount = Math.min(amount, rule.max);
|
|
714
|
+
return amount;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* THRESHOLD: binary trigger — different rates above/below threshold.
|
|
718
|
+
*/
|
|
719
|
+
evalThreshold(rule, ctx) {
|
|
720
|
+
const quantity = ctx.cumulativeQuantity ?? ctx.event.quantity;
|
|
721
|
+
if (quantity <= rule.threshold) {
|
|
722
|
+
return ctx.event.quantity * rule.belowRate;
|
|
723
|
+
}
|
|
724
|
+
return ctx.event.quantity * rule.aboveRate;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* TIME_WINDOW: rate varies by when consumption occurs.
|
|
728
|
+
*/
|
|
729
|
+
evalTimeWindow(rule, ctx) {
|
|
730
|
+
const time = ctx.currentTime ?? ctx.event.timestamp;
|
|
731
|
+
const hour = time.getHours();
|
|
732
|
+
const day = time.getDay();
|
|
733
|
+
for (const window of rule.windows) {
|
|
734
|
+
const dayMatch = !window.dayOfWeek || window.dayOfWeek.includes(day);
|
|
735
|
+
const hourMatch = hour >= window.startHour && hour < window.endHour;
|
|
736
|
+
if (dayMatch && hourMatch) {
|
|
737
|
+
return ctx.event.quantity * window.rate;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return ctx.event.quantity * rule.defaultRate;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* CONDITIONAL: price depends on an attribute or outcome.
|
|
744
|
+
*/
|
|
745
|
+
evalConditional(rule, ctx) {
|
|
746
|
+
const fieldValue = ctx.event.dimensions[rule.field] ?? ctx.attributes?.[rule.field];
|
|
747
|
+
for (const branch of rule.branches) {
|
|
748
|
+
if (this.evaluateCondition(branch.condition, fieldValue)) {
|
|
749
|
+
return this.evaluate(branch.rule, ctx);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (rule.fallback) {
|
|
753
|
+
return this.evaluate(rule.fallback, ctx);
|
|
754
|
+
}
|
|
755
|
+
return 0;
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* COMPOSITE: combines multiple rules with an operator.
|
|
759
|
+
*/
|
|
760
|
+
evalComposite(rule, ctx) {
|
|
761
|
+
const amounts = rule.rules.map((r) => this.evaluate(r, ctx));
|
|
762
|
+
switch (rule.operator) {
|
|
763
|
+
case "ADD":
|
|
764
|
+
return amounts.reduce((sum, a) => sum + a, 0);
|
|
765
|
+
case "MAX":
|
|
766
|
+
return Math.max(...amounts);
|
|
767
|
+
case "MIN":
|
|
768
|
+
return Math.min(...amounts);
|
|
769
|
+
case "FIRST_MATCH":
|
|
770
|
+
return amounts.find((a) => a > 0) ?? 0;
|
|
771
|
+
default:
|
|
772
|
+
throw new UMPError(`Unknown composite operator: ${rule.operator}`, "INVALID_RULE");
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
// ── Condition evaluator (simplified expression engine) ──
|
|
776
|
+
evaluateCondition(condition, value) {
|
|
777
|
+
const eqMatch = condition.match(/^(\w+)\s*==\s*'([^']+)'$/);
|
|
778
|
+
if (eqMatch) return String(value) === eqMatch[2];
|
|
779
|
+
const neqMatch = condition.match(/^(\w+)\s*!=\s*'([^']+)'$/);
|
|
780
|
+
if (neqMatch) return String(value) !== neqMatch[2];
|
|
781
|
+
const gtMatch = condition.match(/^(\w+)\s*>\s*(\d+(?:\.\d+)?)$/);
|
|
782
|
+
if (gtMatch) return Number(value) > Number(gtMatch[2]);
|
|
783
|
+
const ltMatch = condition.match(/^(\w+)\s*<\s*(\d+(?:\.\d+)?)$/);
|
|
784
|
+
if (ltMatch) return Number(value) < Number(ltMatch[2]);
|
|
785
|
+
const gteMatch = condition.match(/^(\w+)\s*>=\s*(\d+(?:\.\d+)?)$/);
|
|
786
|
+
if (gteMatch) return Number(value) >= Number(gteMatch[2]);
|
|
787
|
+
if (condition === String(value)) return true;
|
|
788
|
+
return false;
|
|
789
|
+
}
|
|
790
|
+
// ── Explanation builder ──
|
|
791
|
+
buildExplanation(rule, ctx, depth) {
|
|
792
|
+
const indent = " ".repeat(depth);
|
|
793
|
+
const amount = round(this.evaluate(rule, ctx));
|
|
794
|
+
switch (rule.primitive) {
|
|
795
|
+
case "FIXED":
|
|
796
|
+
return `${indent}FIXED: $${rule.amount} flat = $${amount}`;
|
|
797
|
+
case "UNIT_RATE":
|
|
798
|
+
return `${indent}UNIT_RATE: ${ctx.event.quantity} \xD7 $${rule.rate}/${rule.unit} = $${amount}`;
|
|
799
|
+
case "TIERED":
|
|
800
|
+
return `${indent}TIERED (${rule.mode}): ${ctx.event.quantity} units across ${rule.tiers.length} tiers = $${amount}`;
|
|
801
|
+
case "PERCENTAGE":
|
|
802
|
+
return `${indent}PERCENTAGE: ${(rule.percentage * 100).toFixed(1)}% of reference = $${amount}`;
|
|
803
|
+
case "THRESHOLD":
|
|
804
|
+
return `${indent}THRESHOLD: ${ctx.event.quantity > rule.threshold ? "above" : "below"} ${rule.threshold} \u2192 $${amount}`;
|
|
805
|
+
case "TIME_WINDOW":
|
|
806
|
+
return `${indent}TIME_WINDOW: rate at ${ctx.event.timestamp.toISOString()} = $${amount}`;
|
|
807
|
+
case "CONDITIONAL": {
|
|
808
|
+
const lines = [`${indent}CONDITIONAL on "${rule.field}":`];
|
|
809
|
+
lines.push(`${indent} \u2192 result = $${amount}`);
|
|
810
|
+
return lines.join("\n");
|
|
811
|
+
}
|
|
812
|
+
case "COMPOSITE": {
|
|
813
|
+
const comp = rule;
|
|
814
|
+
const lines = [`${indent}COMPOSITE (${comp.operator}):`];
|
|
815
|
+
for (const sub of comp.rules) {
|
|
816
|
+
lines.push(this.buildExplanation(sub, ctx, depth + 1));
|
|
817
|
+
}
|
|
818
|
+
lines.push(`${indent} = $${amount}`);
|
|
819
|
+
return lines.join("\n");
|
|
820
|
+
}
|
|
821
|
+
default:
|
|
822
|
+
return `${indent}UNKNOWN: $${amount}`;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
// src/terms/contract-manager.ts
|
|
828
|
+
var ContractManager = class {
|
|
829
|
+
contracts = /* @__PURE__ */ new Map();
|
|
830
|
+
/**
|
|
831
|
+
* Create a new contract between two agents.
|
|
832
|
+
*/
|
|
833
|
+
create(sourceAgentId, options) {
|
|
834
|
+
const contractId = generateId("ctr");
|
|
835
|
+
const now = hrTimestamp();
|
|
836
|
+
const rules = options.pricingRules.map((r) => ({
|
|
837
|
+
...r,
|
|
838
|
+
ruleId: r.ruleId || generateId("rul")
|
|
839
|
+
}));
|
|
840
|
+
const contract = {
|
|
841
|
+
contractId,
|
|
842
|
+
mode: options.mode || "TEMPLATE",
|
|
843
|
+
status: "ACTIVE",
|
|
844
|
+
parties: {
|
|
845
|
+
sourceAgentId,
|
|
846
|
+
targetAgentId: options.targetAgentId
|
|
847
|
+
},
|
|
848
|
+
pricingRules: rules,
|
|
849
|
+
effectiveFrom: options.effectiveFrom || now,
|
|
850
|
+
effectiveUntil: options.effectiveUntil,
|
|
851
|
+
metadata: options.metadata || {},
|
|
852
|
+
createdAt: now
|
|
853
|
+
};
|
|
854
|
+
this.contracts.set(contractId, contract);
|
|
855
|
+
return contract;
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* Get contract by ID.
|
|
859
|
+
*/
|
|
860
|
+
get(contractId) {
|
|
861
|
+
const contract = this.contracts.get(contractId);
|
|
862
|
+
if (!contract) throw new ContractNotFoundError(contractId);
|
|
863
|
+
return contract;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Find active contract between two agents.
|
|
867
|
+
*/
|
|
868
|
+
findActive(sourceAgentId, targetAgentId) {
|
|
869
|
+
const now = /* @__PURE__ */ new Date();
|
|
870
|
+
for (const c of this.contracts.values()) {
|
|
871
|
+
if (c.status === "ACTIVE" && c.parties.sourceAgentId === sourceAgentId && c.parties.targetAgentId === targetAgentId && c.effectiveFrom <= now && (!c.effectiveUntil || c.effectiveUntil > now)) {
|
|
872
|
+
return c;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return void 0;
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Dynamic negotiation: propose terms.
|
|
879
|
+
* Returns a DRAFT contract that the counterparty can accept or counter.
|
|
880
|
+
*/
|
|
881
|
+
propose(sourceAgentId, options) {
|
|
882
|
+
const contract = this.create(sourceAgentId, { ...options, mode: "DYNAMIC" });
|
|
883
|
+
contract.status = "PROPOSED";
|
|
884
|
+
return contract;
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Accept a proposed contract — transitions to ACTIVE.
|
|
888
|
+
*/
|
|
889
|
+
accept(contractId) {
|
|
890
|
+
const contract = this.get(contractId);
|
|
891
|
+
if (contract.status !== "PROPOSED") {
|
|
892
|
+
throw new UMPError(`Cannot accept contract in status: ${contract.status}`, "INVALID_STATE");
|
|
893
|
+
}
|
|
894
|
+
contract.status = "ACTIVE";
|
|
895
|
+
return contract;
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Counter-propose: create a new proposal based on an existing one with modified terms.
|
|
899
|
+
*/
|
|
900
|
+
counter(contractId, counterAgentId, modifiedRules) {
|
|
901
|
+
const original = this.get(contractId);
|
|
902
|
+
original.status = "EXPIRED";
|
|
903
|
+
return this.propose(counterAgentId, {
|
|
904
|
+
targetAgentId: original.parties.sourceAgentId,
|
|
905
|
+
// reverse direction
|
|
906
|
+
pricingRules: modifiedRules,
|
|
907
|
+
metadata: {
|
|
908
|
+
...original.metadata,
|
|
909
|
+
counterTo: contractId
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Terminate a contract.
|
|
915
|
+
*/
|
|
916
|
+
terminate(contractId) {
|
|
917
|
+
const contract = this.get(contractId);
|
|
918
|
+
contract.status = "TERMINATED";
|
|
919
|
+
return contract;
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* List contracts for an agent.
|
|
923
|
+
*/
|
|
924
|
+
listByAgent(agentId, status) {
|
|
925
|
+
return Array.from(this.contracts.values()).filter((c) => {
|
|
926
|
+
const partyMatch = c.parties.sourceAgentId === agentId || c.parties.targetAgentId === agentId;
|
|
927
|
+
if (!partyMatch) return false;
|
|
928
|
+
if (status && c.status !== status) return false;
|
|
929
|
+
return true;
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
// src/terms/metering.ts
|
|
935
|
+
var MeteringEngine = class {
|
|
936
|
+
events = /* @__PURE__ */ new Map();
|
|
937
|
+
seenIds = /* @__PURE__ */ new Set();
|
|
938
|
+
// idempotency guard
|
|
939
|
+
/**
|
|
940
|
+
* Record a usage event.
|
|
941
|
+
* Idempotent: resubmitting the same eventId is a no-op.
|
|
942
|
+
*/
|
|
943
|
+
record(event) {
|
|
944
|
+
const eventId = event.eventId || generateId("evt");
|
|
945
|
+
if (this.seenIds.has(eventId)) {
|
|
946
|
+
return this.events.get(eventId);
|
|
947
|
+
}
|
|
948
|
+
const fullEvent = {
|
|
949
|
+
...event,
|
|
950
|
+
eventId,
|
|
951
|
+
timestamp: hrTimestamp()
|
|
952
|
+
};
|
|
953
|
+
this.events.set(eventId, fullEvent);
|
|
954
|
+
this.seenIds.add(eventId);
|
|
955
|
+
return fullEvent;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Record a batch of usage events.
|
|
959
|
+
*/
|
|
960
|
+
recordBatch(events) {
|
|
961
|
+
return events.map((e) => this.record(e));
|
|
962
|
+
}
|
|
963
|
+
/**
|
|
964
|
+
* Create an outcome attestation for result-based billing.
|
|
965
|
+
*/
|
|
966
|
+
attestOutcome(data) {
|
|
967
|
+
return {
|
|
968
|
+
outcomeId: generateId("out"),
|
|
969
|
+
outcomeType: data.outcomeType,
|
|
970
|
+
claimedBy: data.claimedBy,
|
|
971
|
+
evidence: data.evidence,
|
|
972
|
+
verificationMethod: data.verificationMethod,
|
|
973
|
+
verifiedBy: void 0,
|
|
974
|
+
confidenceScore: data.confidenceScore,
|
|
975
|
+
attestationStatus: "CLAIMED",
|
|
976
|
+
disputeWindow: data.disputeWindow || 24 * 60 * 60 * 1e3
|
|
977
|
+
// 24h default
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Get events for a contract within a time range.
|
|
982
|
+
*/
|
|
983
|
+
getByContract(contractId, from, to) {
|
|
984
|
+
let results = Array.from(this.events.values()).filter((e) => e.contractId === contractId);
|
|
985
|
+
if (from) results = results.filter((e) => e.timestamp >= from);
|
|
986
|
+
if (to) results = results.filter((e) => e.timestamp <= to);
|
|
987
|
+
return results.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Get events for a specific agent (as source or target).
|
|
991
|
+
*/
|
|
992
|
+
getByAgent(agentId, limit = 100) {
|
|
993
|
+
return Array.from(this.events.values()).filter((e) => e.sourceAgentId === agentId || e.targetAgentId === agentId).slice(-limit);
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Get a specific event by ID.
|
|
997
|
+
*/
|
|
998
|
+
get(eventId) {
|
|
999
|
+
return this.events.get(eventId);
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Get total metered quantity for a contract.
|
|
1003
|
+
*/
|
|
1004
|
+
totalQuantity(contractId) {
|
|
1005
|
+
return Array.from(this.events.values()).filter((e) => e.contractId === contractId).reduce((sum, e) => sum + e.quantity, 0);
|
|
1006
|
+
}
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
// src/settlement/bus.ts
|
|
1010
|
+
var SettlementBus = class {
|
|
1011
|
+
constructor(wallets, audit, pricingEngine) {
|
|
1012
|
+
this.wallets = wallets;
|
|
1013
|
+
this.audit = audit;
|
|
1014
|
+
this.pricingEngine = pricingEngine;
|
|
1015
|
+
}
|
|
1016
|
+
settlements = /* @__PURE__ */ new Map();
|
|
1017
|
+
escrows = /* @__PURE__ */ new Map();
|
|
1018
|
+
/**
|
|
1019
|
+
* Execute a full transaction: meter → rate → settle in one call.
|
|
1020
|
+
* This is the high-level "transact" method from the Quick Start.
|
|
1021
|
+
*/
|
|
1022
|
+
async transact(sourceAgentId, targetAgentId, event, rule) {
|
|
1023
|
+
const startTime = Date.now();
|
|
1024
|
+
const ratedRecord = this.pricingEngine.rate(rule, { event });
|
|
1025
|
+
return this.settleInstant(
|
|
1026
|
+
sourceAgentId,
|
|
1027
|
+
targetAgentId,
|
|
1028
|
+
[ratedRecord],
|
|
1029
|
+
startTime
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* INSTANT_DRAWDOWN: Debit source, credit target atomically.
|
|
1034
|
+
* Used for per-token, per-inference micro-transactions.
|
|
1035
|
+
*/
|
|
1036
|
+
settleInstant(sourceAgentId, targetAgentId, ratedRecords, _startTime) {
|
|
1037
|
+
const totalAmount = round(ratedRecords.reduce((sum, r) => sum + r.amount, 0));
|
|
1038
|
+
const sourceWallet = this.wallets.getByAgent(sourceAgentId);
|
|
1039
|
+
const targetWallet = this.wallets.getByAgent(targetAgentId);
|
|
1040
|
+
const settlementId = generateId("stl");
|
|
1041
|
+
const txnId = generateId("txn");
|
|
1042
|
+
const balanceBefore = this.wallets.getBalance(sourceWallet.walletId)[0]?.available ?? 0;
|
|
1043
|
+
this.wallets.debit(sourceWallet.walletId, totalAmount, targetAgentId, txnId);
|
|
1044
|
+
this.wallets.credit(targetWallet.walletId, totalAmount, sourceAgentId, txnId);
|
|
1045
|
+
const balanceAfter = this.wallets.getBalance(sourceWallet.walletId)[0]?.available ?? 0;
|
|
1046
|
+
const settlement = {
|
|
1047
|
+
settlementId,
|
|
1048
|
+
pattern: "INSTANT_DRAWDOWN",
|
|
1049
|
+
status: "SETTLED",
|
|
1050
|
+
sourceAgentId,
|
|
1051
|
+
targetAgentId,
|
|
1052
|
+
ratedRecords,
|
|
1053
|
+
totalAmount,
|
|
1054
|
+
currency: ratedRecords[0]?.currency ?? "USD",
|
|
1055
|
+
settledAt: hrTimestamp(),
|
|
1056
|
+
auditId: ""
|
|
1057
|
+
};
|
|
1058
|
+
const auditId = this.audit.record({
|
|
1059
|
+
what: { operation: "SETTLEMENT", entityType: "transaction", entityId: txnId, amount: totalAmount },
|
|
1060
|
+
who: { sourceAgentId, targetAgentId },
|
|
1061
|
+
why: { contractId: ratedRecords[0]?.contractId, pricingRuleId: ratedRecords[0]?.pricingRuleId },
|
|
1062
|
+
how: { policiesEvaluated: ["SPENDING_LIMIT", "COUNTERPARTY_ALLOWLIST"], policiesPassed: ["SPENDING_LIMIT", "COUNTERPARTY_ALLOWLIST"] },
|
|
1063
|
+
result: { balanceBefore, balanceAfter, settlementAmount: totalAmount, status: "SETTLED" }
|
|
1064
|
+
});
|
|
1065
|
+
settlement.auditId = auditId;
|
|
1066
|
+
this.settlements.set(settlementId, settlement);
|
|
1067
|
+
return { settlement, auditId };
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* ESCROW_RELEASE: Reserve funds, release upon outcome attestation.
|
|
1071
|
+
* Used for outcome-based pricing and milestone payments.
|
|
1072
|
+
*/
|
|
1073
|
+
createEscrow(sourceAgentId, targetAgentId, amount, transactionId) {
|
|
1074
|
+
const sourceWallet = this.wallets.getByAgent(sourceAgentId);
|
|
1075
|
+
const targetWallet = this.wallets.getByAgent(targetAgentId);
|
|
1076
|
+
const escrowId = generateId("esc");
|
|
1077
|
+
this.wallets.reserve(sourceWallet.walletId, amount, targetAgentId, transactionId);
|
|
1078
|
+
this.escrows.set(escrowId, {
|
|
1079
|
+
amount,
|
|
1080
|
+
sourceWalletId: sourceWallet.walletId,
|
|
1081
|
+
targetWalletId: targetWallet.walletId
|
|
1082
|
+
});
|
|
1083
|
+
this.audit.record({
|
|
1084
|
+
what: { operation: "ESCROW_CREATED", entityType: "escrow", entityId: escrowId, amount },
|
|
1085
|
+
who: { sourceAgentId, targetAgentId },
|
|
1086
|
+
why: { justification: `Escrow for transaction ${transactionId}` },
|
|
1087
|
+
how: { policiesEvaluated: ["SPENDING_LIMIT"], policiesPassed: ["SPENDING_LIMIT"] },
|
|
1088
|
+
result: { status: "ESCROWED" }
|
|
1089
|
+
});
|
|
1090
|
+
return escrowId;
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Release escrowed funds to the target (full or partial).
|
|
1094
|
+
*/
|
|
1095
|
+
releaseEscrow(escrowId, releaseAmount) {
|
|
1096
|
+
const escrow = this.escrows.get(escrowId);
|
|
1097
|
+
if (!escrow) throw new UMPError(`Escrow not found: ${escrowId}`, "ESCROW_NOT_FOUND");
|
|
1098
|
+
const amount = releaseAmount ?? escrow.amount;
|
|
1099
|
+
const txnId = generateId("txn");
|
|
1100
|
+
this.wallets.releaseReservation(escrow.sourceWalletId, amount, txnId);
|
|
1101
|
+
this.wallets.credit(escrow.targetWalletId, amount, "escrow", txnId);
|
|
1102
|
+
if (amount < escrow.amount) {
|
|
1103
|
+
escrow.amount -= amount;
|
|
1104
|
+
} else {
|
|
1105
|
+
this.escrows.delete(escrowId);
|
|
1106
|
+
}
|
|
1107
|
+
const settlement = {
|
|
1108
|
+
settlementId: generateId("stl"),
|
|
1109
|
+
pattern: "ESCROW_RELEASE",
|
|
1110
|
+
status: "SETTLED",
|
|
1111
|
+
sourceAgentId: "escrow",
|
|
1112
|
+
targetAgentId: "escrow",
|
|
1113
|
+
ratedRecords: [],
|
|
1114
|
+
totalAmount: amount,
|
|
1115
|
+
currency: "USD",
|
|
1116
|
+
settledAt: hrTimestamp(),
|
|
1117
|
+
auditId: ""
|
|
1118
|
+
};
|
|
1119
|
+
const auditId = this.audit.record({
|
|
1120
|
+
what: { operation: "ESCROW_RELEASED", entityType: "escrow", entityId: escrowId, amount },
|
|
1121
|
+
who: { sourceAgentId: "escrow", targetAgentId: "escrow" },
|
|
1122
|
+
why: { justification: `Escrow ${escrowId} released` },
|
|
1123
|
+
how: { policiesEvaluated: ["OUTCOME_VERIFICATION"], policiesPassed: ["OUTCOME_VERIFICATION"] },
|
|
1124
|
+
result: { settlementAmount: amount, status: "SETTLED" }
|
|
1125
|
+
});
|
|
1126
|
+
settlement.auditId = auditId;
|
|
1127
|
+
this.settlements.set(settlement.settlementId, settlement);
|
|
1128
|
+
return { settlement, auditId };
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* WATERFALL_SPLIT: Distribute payment across multiple parties.
|
|
1132
|
+
* Used for marketplace commissions and multi-party revenue share.
|
|
1133
|
+
*/
|
|
1134
|
+
settleWaterfall(sourceAgentId, splits, ratedRecords) {
|
|
1135
|
+
const sourceWallet = this.wallets.getByAgent(sourceAgentId);
|
|
1136
|
+
const totalAmount = round(splits.reduce((sum, s) => sum + s.amount, 0));
|
|
1137
|
+
const txnId = generateId("txn");
|
|
1138
|
+
this.wallets.debit(sourceWallet.walletId, totalAmount, "waterfall", txnId);
|
|
1139
|
+
const results = splits.map((split) => {
|
|
1140
|
+
const targetWallet = this.wallets.getByAgent(split.agentId);
|
|
1141
|
+
this.wallets.credit(targetWallet.walletId, split.amount, sourceAgentId, txnId);
|
|
1142
|
+
const settlement = {
|
|
1143
|
+
settlementId: generateId("stl"),
|
|
1144
|
+
pattern: "WATERFALL_SPLIT",
|
|
1145
|
+
status: "SETTLED",
|
|
1146
|
+
sourceAgentId,
|
|
1147
|
+
targetAgentId: split.agentId,
|
|
1148
|
+
ratedRecords,
|
|
1149
|
+
totalAmount: split.amount,
|
|
1150
|
+
currency: "USD",
|
|
1151
|
+
settledAt: hrTimestamp(),
|
|
1152
|
+
auditId: ""
|
|
1153
|
+
};
|
|
1154
|
+
const auditId = this.audit.record({
|
|
1155
|
+
what: { operation: "WATERFALL_SPLIT", entityType: "settlement", entityId: settlement.settlementId, amount: split.amount },
|
|
1156
|
+
who: { sourceAgentId, targetAgentId: split.agentId },
|
|
1157
|
+
why: { justification: `Waterfall split: ${split.amount} to ${split.agentId}` },
|
|
1158
|
+
how: { policiesEvaluated: ["SPENDING_LIMIT"], policiesPassed: ["SPENDING_LIMIT"] },
|
|
1159
|
+
result: { settlementAmount: split.amount, status: "SETTLED" }
|
|
1160
|
+
});
|
|
1161
|
+
settlement.auditId = auditId;
|
|
1162
|
+
this.settlements.set(settlement.settlementId, settlement);
|
|
1163
|
+
return { settlement, auditId };
|
|
1164
|
+
});
|
|
1165
|
+
return results;
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Get settlement by ID.
|
|
1169
|
+
*/
|
|
1170
|
+
get(settlementId) {
|
|
1171
|
+
return this.settlements.get(settlementId);
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* List settlements for an agent.
|
|
1175
|
+
*/
|
|
1176
|
+
listByAgent(agentId, limit = 50) {
|
|
1177
|
+
return Array.from(this.settlements.values()).filter((s) => s.sourceAgentId === agentId || s.targetAgentId === agentId).slice(-limit);
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
// src/pricing/templates.ts
|
|
1182
|
+
var PricingTemplates = {
|
|
1183
|
+
/**
|
|
1184
|
+
* Per-Token LLM pricing (separate input/output rates).
|
|
1185
|
+
* Example: GPT-4 at $30/M input, $60/M output tokens.
|
|
1186
|
+
*/
|
|
1187
|
+
perToken(inputRatePerMillion, outputRatePerMillion) {
|
|
1188
|
+
return {
|
|
1189
|
+
ruleId: generateId("rul"),
|
|
1190
|
+
name: "Per-Token LLM",
|
|
1191
|
+
description: `$${inputRatePerMillion}/M input, $${outputRatePerMillion}/M output tokens`,
|
|
1192
|
+
primitive: "CONDITIONAL",
|
|
1193
|
+
field: "direction",
|
|
1194
|
+
branches: [
|
|
1195
|
+
{
|
|
1196
|
+
condition: "direction == 'input'",
|
|
1197
|
+
rule: {
|
|
1198
|
+
ruleId: generateId("rul"),
|
|
1199
|
+
name: "Input Token Rate",
|
|
1200
|
+
primitive: "UNIT_RATE",
|
|
1201
|
+
rate: inputRatePerMillion / 1e6,
|
|
1202
|
+
unit: "TOKEN"
|
|
1203
|
+
}
|
|
1204
|
+
},
|
|
1205
|
+
{
|
|
1206
|
+
condition: "direction == 'output'",
|
|
1207
|
+
rule: {
|
|
1208
|
+
ruleId: generateId("rul"),
|
|
1209
|
+
name: "Output Token Rate",
|
|
1210
|
+
primitive: "UNIT_RATE",
|
|
1211
|
+
rate: outputRatePerMillion / 1e6,
|
|
1212
|
+
unit: "TOKEN"
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
]
|
|
1216
|
+
};
|
|
1217
|
+
},
|
|
1218
|
+
/**
|
|
1219
|
+
* Per-Inference pricing with volume tiers.
|
|
1220
|
+
* Example: $0.01/call for first 10K, $0.005/call after.
|
|
1221
|
+
*/
|
|
1222
|
+
perInference(tiers) {
|
|
1223
|
+
return {
|
|
1224
|
+
ruleId: generateId("rul"),
|
|
1225
|
+
name: "Per-Inference Tiered",
|
|
1226
|
+
primitive: "TIERED",
|
|
1227
|
+
mode: "GRADUATED",
|
|
1228
|
+
tiers: tiers.map((t, i) => ({
|
|
1229
|
+
from: i === 0 ? 0 : tiers[i - 1].upTo ?? 0,
|
|
1230
|
+
to: t.upTo,
|
|
1231
|
+
rate: t.rate
|
|
1232
|
+
}))
|
|
1233
|
+
};
|
|
1234
|
+
},
|
|
1235
|
+
/**
|
|
1236
|
+
* Per-Resolution / outcome-based pricing.
|
|
1237
|
+
* Charges only on successful outcome + percentage of value.
|
|
1238
|
+
*/
|
|
1239
|
+
perResolution(successFee, valuePct) {
|
|
1240
|
+
return {
|
|
1241
|
+
ruleId: generateId("rul"),
|
|
1242
|
+
name: "Per-Resolution Outcome",
|
|
1243
|
+
primitive: "CONDITIONAL",
|
|
1244
|
+
field: "outcome",
|
|
1245
|
+
branches: [
|
|
1246
|
+
{
|
|
1247
|
+
condition: "outcome == 'SUCCESS'",
|
|
1248
|
+
rule: {
|
|
1249
|
+
ruleId: generateId("rul"),
|
|
1250
|
+
name: "Success Composite",
|
|
1251
|
+
primitive: "COMPOSITE",
|
|
1252
|
+
operator: "ADD",
|
|
1253
|
+
rules: [
|
|
1254
|
+
{
|
|
1255
|
+
ruleId: generateId("rul"),
|
|
1256
|
+
name: "Success Fee",
|
|
1257
|
+
primitive: "FIXED",
|
|
1258
|
+
amount: successFee,
|
|
1259
|
+
period: "PER_EVENT"
|
|
1260
|
+
},
|
|
1261
|
+
{
|
|
1262
|
+
ruleId: generateId("rul"),
|
|
1263
|
+
name: "Value Share",
|
|
1264
|
+
primitive: "PERCENTAGE",
|
|
1265
|
+
percentage: valuePct,
|
|
1266
|
+
referenceField: "value_generated"
|
|
1267
|
+
}
|
|
1268
|
+
]
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
],
|
|
1272
|
+
fallback: {
|
|
1273
|
+
ruleId: generateId("rul"),
|
|
1274
|
+
name: "No charge on failure",
|
|
1275
|
+
primitive: "FIXED",
|
|
1276
|
+
amount: 0,
|
|
1277
|
+
period: "PER_EVENT"
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
},
|
|
1281
|
+
/**
|
|
1282
|
+
* Subscription + Usage hybrid.
|
|
1283
|
+
* Base monthly fee + per-unit overage above included units.
|
|
1284
|
+
*/
|
|
1285
|
+
subscriptionPlusUsage(monthlyBase, includedUnits, overageRate) {
|
|
1286
|
+
return {
|
|
1287
|
+
ruleId: generateId("rul"),
|
|
1288
|
+
name: "Subscription + Usage",
|
|
1289
|
+
primitive: "COMPOSITE",
|
|
1290
|
+
operator: "ADD",
|
|
1291
|
+
rules: [
|
|
1292
|
+
{
|
|
1293
|
+
ruleId: generateId("rul"),
|
|
1294
|
+
name: "Monthly Base",
|
|
1295
|
+
primitive: "FIXED",
|
|
1296
|
+
amount: monthlyBase,
|
|
1297
|
+
period: "MONTHLY"
|
|
1298
|
+
},
|
|
1299
|
+
{
|
|
1300
|
+
ruleId: generateId("rul"),
|
|
1301
|
+
name: "Overage",
|
|
1302
|
+
primitive: "THRESHOLD",
|
|
1303
|
+
threshold: includedUnits,
|
|
1304
|
+
belowRate: 0,
|
|
1305
|
+
aboveRate: overageRate
|
|
1306
|
+
}
|
|
1307
|
+
]
|
|
1308
|
+
};
|
|
1309
|
+
},
|
|
1310
|
+
/**
|
|
1311
|
+
* Credit pool with per-unit drawdown.
|
|
1312
|
+
*/
|
|
1313
|
+
creditPool(creditCost) {
|
|
1314
|
+
return {
|
|
1315
|
+
ruleId: generateId("rul"),
|
|
1316
|
+
name: "Credit Pool",
|
|
1317
|
+
primitive: "UNIT_RATE",
|
|
1318
|
+
rate: creditCost,
|
|
1319
|
+
unit: "CREDIT"
|
|
1320
|
+
};
|
|
1321
|
+
},
|
|
1322
|
+
/**
|
|
1323
|
+
* Spot compute pricing with peak/off-peak rates.
|
|
1324
|
+
*/
|
|
1325
|
+
computeSpot(peakRate, offPeakRate, peakHours = [9, 17]) {
|
|
1326
|
+
return {
|
|
1327
|
+
ruleId: generateId("rul"),
|
|
1328
|
+
name: "Compute Spot",
|
|
1329
|
+
primitive: "TIME_WINDOW",
|
|
1330
|
+
windows: [
|
|
1331
|
+
{
|
|
1332
|
+
startHour: peakHours[0],
|
|
1333
|
+
endHour: peakHours[1],
|
|
1334
|
+
rate: peakRate,
|
|
1335
|
+
label: "peak",
|
|
1336
|
+
dayOfWeek: [1, 2, 3, 4, 5]
|
|
1337
|
+
// weekdays
|
|
1338
|
+
}
|
|
1339
|
+
],
|
|
1340
|
+
defaultRate: offPeakRate,
|
|
1341
|
+
timezone: "UTC"
|
|
1342
|
+
};
|
|
1343
|
+
},
|
|
1344
|
+
/**
|
|
1345
|
+
* Marketplace commission (percentage take rate with floor and cap).
|
|
1346
|
+
*/
|
|
1347
|
+
marketplaceCommission(takeRate, minFee, maxFee) {
|
|
1348
|
+
return {
|
|
1349
|
+
ruleId: generateId("rul"),
|
|
1350
|
+
name: "Marketplace Commission",
|
|
1351
|
+
primitive: "PERCENTAGE",
|
|
1352
|
+
percentage: takeRate,
|
|
1353
|
+
referenceField: "transaction_amount",
|
|
1354
|
+
min: minFee,
|
|
1355
|
+
max: maxFee
|
|
1356
|
+
};
|
|
1357
|
+
},
|
|
1358
|
+
/**
|
|
1359
|
+
* Agent-to-agent task pricing (fixed per-task + outcome bonus).
|
|
1360
|
+
*/
|
|
1361
|
+
agentTask(basePerTask, bonusOnSuccess) {
|
|
1362
|
+
return {
|
|
1363
|
+
ruleId: generateId("rul"),
|
|
1364
|
+
name: "Agent Task",
|
|
1365
|
+
primitive: "COMPOSITE",
|
|
1366
|
+
operator: "ADD",
|
|
1367
|
+
rules: [
|
|
1368
|
+
{
|
|
1369
|
+
ruleId: generateId("rul"),
|
|
1370
|
+
name: "Base Task Fee",
|
|
1371
|
+
primitive: "FIXED",
|
|
1372
|
+
amount: basePerTask,
|
|
1373
|
+
period: "PER_EVENT"
|
|
1374
|
+
},
|
|
1375
|
+
{
|
|
1376
|
+
ruleId: generateId("rul"),
|
|
1377
|
+
name: "Success Bonus",
|
|
1378
|
+
primitive: "CONDITIONAL",
|
|
1379
|
+
field: "outcome",
|
|
1380
|
+
branches: [
|
|
1381
|
+
{
|
|
1382
|
+
condition: "outcome == 'SUCCESS'",
|
|
1383
|
+
rule: {
|
|
1384
|
+
ruleId: generateId("rul"),
|
|
1385
|
+
name: "Bonus",
|
|
1386
|
+
primitive: "FIXED",
|
|
1387
|
+
amount: bonusOnSuccess,
|
|
1388
|
+
period: "PER_EVENT"
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
]
|
|
1392
|
+
}
|
|
1393
|
+
]
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
};
|
|
1397
|
+
|
|
1398
|
+
// src/index.ts
|
|
1399
|
+
var UMP = class {
|
|
1400
|
+
// ── Layer 1: Identity & Value ──
|
|
1401
|
+
agents;
|
|
1402
|
+
wallets;
|
|
1403
|
+
// ── Layer 2: Terms & Metering ──
|
|
1404
|
+
contracts;
|
|
1405
|
+
metering;
|
|
1406
|
+
pricing;
|
|
1407
|
+
// ── Layer 3: Settlement & Governance ──
|
|
1408
|
+
settlement;
|
|
1409
|
+
audit;
|
|
1410
|
+
constructor(config) {
|
|
1411
|
+
this.agents = new AgentManager();
|
|
1412
|
+
this.wallets = new WalletManager();
|
|
1413
|
+
this.audit = new AuditTrail(config.onAudit);
|
|
1414
|
+
this.pricing = new PricingEngine();
|
|
1415
|
+
this.contracts = new ContractManager();
|
|
1416
|
+
this.metering = new MeteringEngine();
|
|
1417
|
+
this.settlement = new SettlementBus(this.wallets, this.audit, this.pricing);
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* High-level: Register an agent and create its wallet.
|
|
1421
|
+
* Returns an AgentHandle with convenience methods.
|
|
1422
|
+
*/
|
|
1423
|
+
registerAgent(options) {
|
|
1424
|
+
const agent = this.agents.create(options);
|
|
1425
|
+
const wallet = this.wallets.create(agent.agentId);
|
|
1426
|
+
return {
|
|
1427
|
+
id: agent.agentId,
|
|
1428
|
+
agent,
|
|
1429
|
+
wallet: {
|
|
1430
|
+
fund: (opts) => {
|
|
1431
|
+
this.wallets.fund(wallet.walletId, opts);
|
|
1432
|
+
},
|
|
1433
|
+
balance: () => {
|
|
1434
|
+
const balances = this.wallets.getBalance(wallet.walletId);
|
|
1435
|
+
return balances[0]?.available ?? 0;
|
|
1436
|
+
},
|
|
1437
|
+
freeze: () => this.wallets.freeze(wallet.walletId),
|
|
1438
|
+
unfreeze: () => this.wallets.unfreeze(wallet.walletId)
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
/**
|
|
1443
|
+
* High-level: Execute a priced transaction between two agents.
|
|
1444
|
+
*
|
|
1445
|
+
* This is the "10 lines of code" Quick Start method.
|
|
1446
|
+
* It finds or creates a contract, meters the event, rates it,
|
|
1447
|
+
* settles payment, and returns the result — all in one call.
|
|
1448
|
+
*/
|
|
1449
|
+
async transact(options) {
|
|
1450
|
+
const startTime = Date.now();
|
|
1451
|
+
const txnId = generateId("txn");
|
|
1452
|
+
const sourceCheck = this.agents.verify(options.from);
|
|
1453
|
+
if (!sourceCheck.valid) {
|
|
1454
|
+
throw new AgentRevokedError(options.from);
|
|
1455
|
+
}
|
|
1456
|
+
let contract = this.contracts.findActive(options.from, options.to);
|
|
1457
|
+
if (!contract) {
|
|
1458
|
+
contract = this.contracts.create(options.from, {
|
|
1459
|
+
targetAgentId: options.to,
|
|
1460
|
+
pricingRules: [{
|
|
1461
|
+
name: "Default per-unit",
|
|
1462
|
+
primitive: "UNIT_RATE"
|
|
1463
|
+
}]
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
const maxCost = options.maxCost ? parseMoney(options.maxCost) : void 0;
|
|
1467
|
+
if (maxCost) {
|
|
1468
|
+
const authCheck = this.agents.checkAuthority(options.from, maxCost, options.to, options.service);
|
|
1469
|
+
if (!authCheck.allowed) {
|
|
1470
|
+
throw new UMPError(`Authority check failed: ${authCheck.reason}`, "AUTHORITY_EXCEEDED");
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
const event = this.metering.record({
|
|
1474
|
+
sourceAgentId: options.from,
|
|
1475
|
+
targetAgentId: options.to,
|
|
1476
|
+
contractId: contract.contractId,
|
|
1477
|
+
serviceId: options.service,
|
|
1478
|
+
quantity: 1,
|
|
1479
|
+
unit: "API_CALL",
|
|
1480
|
+
dimensions: options.payload || {}
|
|
1481
|
+
});
|
|
1482
|
+
const rule = contract.pricingRules[0];
|
|
1483
|
+
const { settlement, auditId } = await this.settlement.transact(
|
|
1484
|
+
options.from,
|
|
1485
|
+
options.to,
|
|
1486
|
+
event,
|
|
1487
|
+
rule
|
|
1488
|
+
);
|
|
1489
|
+
return {
|
|
1490
|
+
transactionId: txnId,
|
|
1491
|
+
cost: settlement.totalAmount,
|
|
1492
|
+
currency: settlement.currency,
|
|
1493
|
+
outcome: event.outcome,
|
|
1494
|
+
auditId,
|
|
1495
|
+
settledAt: settlement.settledAt || /* @__PURE__ */ new Date(),
|
|
1496
|
+
duration: Date.now() - startTime
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1501
|
+
0 && (module.exports = {
|
|
1502
|
+
AgentManager,
|
|
1503
|
+
AgentNotFoundError,
|
|
1504
|
+
AgentRevokedError,
|
|
1505
|
+
AuditTrail,
|
|
1506
|
+
AuthorityExceededError,
|
|
1507
|
+
ContractManager,
|
|
1508
|
+
ContractNotFoundError,
|
|
1509
|
+
DisputeError,
|
|
1510
|
+
InsufficientFundsError,
|
|
1511
|
+
MeteringEngine,
|
|
1512
|
+
PolicyViolationError,
|
|
1513
|
+
PricingEngine,
|
|
1514
|
+
PricingTemplates,
|
|
1515
|
+
SettlementBus,
|
|
1516
|
+
UMP,
|
|
1517
|
+
UMPError,
|
|
1518
|
+
WalletFrozenError,
|
|
1519
|
+
WalletManager
|
|
1520
|
+
});
|