@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.
@@ -0,0 +1,253 @@
1
+ import type {
2
+ Settlement,
3
+ RatedRecord, UsageEvent, PricingRule,
4
+ } from '../types';
5
+ import { generateId, round, hrTimestamp } from '../utils/id';
6
+ import { WalletManager } from '../core/wallet-manager';
7
+ import { AuditTrail } from '../core/audit-trail';
8
+ import { PricingEngine } from '../pricing/engine';
9
+ import { UMPError } from '../utils/errors';
10
+
11
+ export interface SettlementResult {
12
+ settlement: Settlement;
13
+ auditId: string;
14
+ }
15
+
16
+ /**
17
+ * SettlementBus — Layer 3 primitive
18
+ *
19
+ * Executes real-time settlement between agent wallets.
20
+ * Supports 6 patterns: instant drawdown, escrow, waterfall,
21
+ * net settlement, conditional release, cross-currency atomic.
22
+ */
23
+ export class SettlementBus {
24
+ private settlements: Map<string, Settlement> = new Map();
25
+ private escrows: Map<string, { amount: number; sourceWalletId: string; targetWalletId: string }> = new Map();
26
+
27
+ constructor(
28
+ private wallets: WalletManager,
29
+ private audit: AuditTrail,
30
+ private pricingEngine: PricingEngine,
31
+ ) {}
32
+
33
+ /**
34
+ * Execute a full transaction: meter → rate → settle in one call.
35
+ * This is the high-level "transact" method from the Quick Start.
36
+ */
37
+ async transact(
38
+ sourceAgentId: string,
39
+ targetAgentId: string,
40
+ event: UsageEvent,
41
+ rule: PricingRule,
42
+ ): Promise<SettlementResult> {
43
+ const startTime = Date.now();
44
+
45
+ // 1. Rate the usage event
46
+ const ratedRecord = this.pricingEngine.rate(rule, { event });
47
+
48
+ // 2. Execute instant drawdown settlement
49
+ return this.settleInstant(
50
+ sourceAgentId,
51
+ targetAgentId,
52
+ [ratedRecord],
53
+ startTime,
54
+ );
55
+ }
56
+
57
+ /**
58
+ * INSTANT_DRAWDOWN: Debit source, credit target atomically.
59
+ * Used for per-token, per-inference micro-transactions.
60
+ */
61
+ settleInstant(
62
+ sourceAgentId: string,
63
+ targetAgentId: string,
64
+ ratedRecords: RatedRecord[],
65
+ _startTime?: number,
66
+ ): SettlementResult {
67
+ const totalAmount = round(ratedRecords.reduce((sum, r) => sum + r.amount, 0));
68
+ const sourceWallet = this.wallets.getByAgent(sourceAgentId);
69
+ const targetWallet = this.wallets.getByAgent(targetAgentId);
70
+ const settlementId = generateId('stl');
71
+ const txnId = generateId('txn');
72
+
73
+ // Atomic: debit source, credit target
74
+ const balanceBefore = this.wallets.getBalance(sourceWallet.walletId)[0]?.available ?? 0;
75
+ this.wallets.debit(sourceWallet.walletId, totalAmount, targetAgentId, txnId);
76
+ this.wallets.credit(targetWallet.walletId, totalAmount, sourceAgentId, txnId);
77
+ const balanceAfter = this.wallets.getBalance(sourceWallet.walletId)[0]?.available ?? 0;
78
+
79
+ const settlement: Settlement = {
80
+ settlementId,
81
+ pattern: 'INSTANT_DRAWDOWN',
82
+ status: 'SETTLED',
83
+ sourceAgentId,
84
+ targetAgentId,
85
+ ratedRecords,
86
+ totalAmount,
87
+ currency: ratedRecords[0]?.currency ?? 'USD',
88
+ settledAt: hrTimestamp(),
89
+ auditId: '',
90
+ };
91
+
92
+ // Audit
93
+ const auditId = this.audit.record({
94
+ what: { operation: 'SETTLEMENT', entityType: 'transaction', entityId: txnId, amount: totalAmount },
95
+ who: { sourceAgentId, targetAgentId },
96
+ why: { contractId: ratedRecords[0]?.contractId, pricingRuleId: ratedRecords[0]?.pricingRuleId },
97
+ how: { policiesEvaluated: ['SPENDING_LIMIT', 'COUNTERPARTY_ALLOWLIST'], policiesPassed: ['SPENDING_LIMIT', 'COUNTERPARTY_ALLOWLIST'] },
98
+ result: { balanceBefore, balanceAfter, settlementAmount: totalAmount, status: 'SETTLED' },
99
+ });
100
+
101
+ settlement.auditId = auditId;
102
+ this.settlements.set(settlementId, settlement);
103
+
104
+ return { settlement, auditId };
105
+ }
106
+
107
+ /**
108
+ * ESCROW_RELEASE: Reserve funds, release upon outcome attestation.
109
+ * Used for outcome-based pricing and milestone payments.
110
+ */
111
+ createEscrow(
112
+ sourceAgentId: string,
113
+ targetAgentId: string,
114
+ amount: number,
115
+ transactionId: string,
116
+ ): string {
117
+ const sourceWallet = this.wallets.getByAgent(sourceAgentId);
118
+ const targetWallet = this.wallets.getByAgent(targetAgentId);
119
+ const escrowId = generateId('esc');
120
+
121
+ // Reserve in source wallet
122
+ this.wallets.reserve(sourceWallet.walletId, amount, targetAgentId, transactionId);
123
+
124
+ this.escrows.set(escrowId, {
125
+ amount,
126
+ sourceWalletId: sourceWallet.walletId,
127
+ targetWalletId: targetWallet.walletId,
128
+ });
129
+
130
+ this.audit.record({
131
+ what: { operation: 'ESCROW_CREATED', entityType: 'escrow', entityId: escrowId, amount },
132
+ who: { sourceAgentId, targetAgentId },
133
+ why: { justification: `Escrow for transaction ${transactionId}` },
134
+ how: { policiesEvaluated: ['SPENDING_LIMIT'], policiesPassed: ['SPENDING_LIMIT'] },
135
+ result: { status: 'ESCROWED' },
136
+ });
137
+
138
+ return escrowId;
139
+ }
140
+
141
+ /**
142
+ * Release escrowed funds to the target (full or partial).
143
+ */
144
+ releaseEscrow(escrowId: string, releaseAmount?: number): SettlementResult {
145
+ const escrow = this.escrows.get(escrowId);
146
+ if (!escrow) throw new UMPError(`Escrow not found: ${escrowId}`, 'ESCROW_NOT_FOUND');
147
+
148
+ const amount = releaseAmount ?? escrow.amount;
149
+ const txnId = generateId('txn');
150
+
151
+ // Release reservation from source, credit target
152
+ this.wallets.releaseReservation(escrow.sourceWalletId, amount, txnId);
153
+ this.wallets.credit(escrow.targetWalletId, amount, 'escrow', txnId);
154
+
155
+ // If partial release, update remaining
156
+ if (amount < escrow.amount) {
157
+ escrow.amount -= amount;
158
+ } else {
159
+ this.escrows.delete(escrowId);
160
+ }
161
+
162
+ const settlement: Settlement = {
163
+ settlementId: generateId('stl'),
164
+ pattern: 'ESCROW_RELEASE',
165
+ status: 'SETTLED',
166
+ sourceAgentId: 'escrow',
167
+ targetAgentId: 'escrow',
168
+ ratedRecords: [],
169
+ totalAmount: amount,
170
+ currency: 'USD',
171
+ settledAt: hrTimestamp(),
172
+ auditId: '',
173
+ };
174
+
175
+ const auditId = this.audit.record({
176
+ what: { operation: 'ESCROW_RELEASED', entityType: 'escrow', entityId: escrowId, amount },
177
+ who: { sourceAgentId: 'escrow', targetAgentId: 'escrow' },
178
+ why: { justification: `Escrow ${escrowId} released` },
179
+ how: { policiesEvaluated: ['OUTCOME_VERIFICATION'], policiesPassed: ['OUTCOME_VERIFICATION'] },
180
+ result: { settlementAmount: amount, status: 'SETTLED' },
181
+ });
182
+
183
+ settlement.auditId = auditId;
184
+ this.settlements.set(settlement.settlementId, settlement);
185
+ return { settlement, auditId };
186
+ }
187
+
188
+ /**
189
+ * WATERFALL_SPLIT: Distribute payment across multiple parties.
190
+ * Used for marketplace commissions and multi-party revenue share.
191
+ */
192
+ settleWaterfall(
193
+ sourceAgentId: string,
194
+ splits: Array<{ agentId: string; amount: number }>,
195
+ ratedRecords: RatedRecord[],
196
+ ): SettlementResult[] {
197
+ const sourceWallet = this.wallets.getByAgent(sourceAgentId);
198
+ const totalAmount = round(splits.reduce((sum, s) => sum + s.amount, 0));
199
+ const txnId = generateId('txn');
200
+
201
+ // Debit total from source
202
+ this.wallets.debit(sourceWallet.walletId, totalAmount, 'waterfall', txnId);
203
+
204
+ // Credit each target
205
+ const results: SettlementResult[] = splits.map(split => {
206
+ const targetWallet = this.wallets.getByAgent(split.agentId);
207
+ this.wallets.credit(targetWallet.walletId, split.amount, sourceAgentId, txnId);
208
+
209
+ const settlement: Settlement = {
210
+ settlementId: generateId('stl'),
211
+ pattern: 'WATERFALL_SPLIT',
212
+ status: 'SETTLED',
213
+ sourceAgentId,
214
+ targetAgentId: split.agentId,
215
+ ratedRecords,
216
+ totalAmount: split.amount,
217
+ currency: 'USD',
218
+ settledAt: hrTimestamp(),
219
+ auditId: '',
220
+ };
221
+
222
+ const auditId = this.audit.record({
223
+ what: { operation: 'WATERFALL_SPLIT', entityType: 'settlement', entityId: settlement.settlementId, amount: split.amount },
224
+ who: { sourceAgentId, targetAgentId: split.agentId },
225
+ why: { justification: `Waterfall split: ${split.amount} to ${split.agentId}` },
226
+ how: { policiesEvaluated: ['SPENDING_LIMIT'], policiesPassed: ['SPENDING_LIMIT'] },
227
+ result: { settlementAmount: split.amount, status: 'SETTLED' },
228
+ });
229
+
230
+ settlement.auditId = auditId;
231
+ this.settlements.set(settlement.settlementId, settlement);
232
+ return { settlement, auditId };
233
+ });
234
+
235
+ return results;
236
+ }
237
+
238
+ /**
239
+ * Get settlement by ID.
240
+ */
241
+ get(settlementId: string): Settlement | undefined {
242
+ return this.settlements.get(settlementId);
243
+ }
244
+
245
+ /**
246
+ * List settlements for an agent.
247
+ */
248
+ listByAgent(agentId: string, limit = 50): Settlement[] {
249
+ return Array.from(this.settlements.values())
250
+ .filter(s => s.sourceAgentId === agentId || s.targetAgentId === agentId)
251
+ .slice(-limit);
252
+ }
253
+ }
@@ -0,0 +1,2 @@
1
+ export { SettlementBus } from './bus';
2
+ export type { SettlementResult } from './bus';
@@ -0,0 +1,142 @@
1
+ import type {
2
+ Contract, ContractStatus,
3
+ CreateContractOptions, PricingRule,
4
+ } from '../types';
5
+ import { generateId, hrTimestamp } from '../utils/id';
6
+ import { ContractNotFoundError, UMPError } from '../utils/errors';
7
+
8
+ /**
9
+ * ContractManager — Layer 1/2 bridge
10
+ *
11
+ * Manages commercial agreements between agents.
12
+ * Supports pre-negotiated, template-based, and dynamic negotiation.
13
+ */
14
+ export class ContractManager {
15
+ private contracts: Map<string, Contract> = new Map();
16
+
17
+ /**
18
+ * Create a new contract between two agents.
19
+ */
20
+ create(sourceAgentId: string, options: CreateContractOptions): Contract {
21
+ const contractId = generateId('ctr');
22
+ const now = hrTimestamp();
23
+
24
+ // Assign rule IDs to any rules that don't have them
25
+ const rules: PricingRule[] = options.pricingRules.map(r => ({
26
+ ...r,
27
+ ruleId: (r as PricingRule).ruleId || generateId('rul'),
28
+ })) as PricingRule[];
29
+
30
+ const contract: Contract = {
31
+ contractId,
32
+ mode: options.mode || 'TEMPLATE',
33
+ status: 'ACTIVE',
34
+ parties: {
35
+ sourceAgentId,
36
+ targetAgentId: options.targetAgentId,
37
+ },
38
+ pricingRules: rules,
39
+ effectiveFrom: options.effectiveFrom || now,
40
+ effectiveUntil: options.effectiveUntil,
41
+ metadata: options.metadata || {},
42
+ createdAt: now,
43
+ };
44
+
45
+ this.contracts.set(contractId, contract);
46
+ return contract;
47
+ }
48
+
49
+ /**
50
+ * Get contract by ID.
51
+ */
52
+ get(contractId: string): Contract {
53
+ const contract = this.contracts.get(contractId);
54
+ if (!contract) throw new ContractNotFoundError(contractId);
55
+ return contract;
56
+ }
57
+
58
+ /**
59
+ * Find active contract between two agents.
60
+ */
61
+ findActive(sourceAgentId: string, targetAgentId: string): Contract | undefined {
62
+ const now = new Date();
63
+ for (const c of this.contracts.values()) {
64
+ if (
65
+ c.status === 'ACTIVE' &&
66
+ c.parties.sourceAgentId === sourceAgentId &&
67
+ c.parties.targetAgentId === targetAgentId &&
68
+ c.effectiveFrom <= now &&
69
+ (!c.effectiveUntil || c.effectiveUntil > now)
70
+ ) {
71
+ return c;
72
+ }
73
+ }
74
+ return undefined;
75
+ }
76
+
77
+ /**
78
+ * Dynamic negotiation: propose terms.
79
+ * Returns a DRAFT contract that the counterparty can accept or counter.
80
+ */
81
+ propose(sourceAgentId: string, options: CreateContractOptions): Contract {
82
+ const contract = this.create(sourceAgentId, { ...options, mode: 'DYNAMIC' });
83
+ contract.status = 'PROPOSED';
84
+ return contract;
85
+ }
86
+
87
+ /**
88
+ * Accept a proposed contract — transitions to ACTIVE.
89
+ */
90
+ accept(contractId: string): Contract {
91
+ const contract = this.get(contractId);
92
+ if (contract.status !== 'PROPOSED') {
93
+ throw new UMPError(`Cannot accept contract in status: ${contract.status}`, 'INVALID_STATE');
94
+ }
95
+ contract.status = 'ACTIVE';
96
+ return contract;
97
+ }
98
+
99
+ /**
100
+ * Counter-propose: create a new proposal based on an existing one with modified terms.
101
+ */
102
+ counter(
103
+ contractId: string,
104
+ counterAgentId: string,
105
+ modifiedRules: Omit<PricingRule, 'ruleId'>[],
106
+ ): Contract {
107
+ const original = this.get(contractId);
108
+ original.status = 'EXPIRED'; // superseded
109
+
110
+ return this.propose(counterAgentId, {
111
+ targetAgentId: original.parties.sourceAgentId, // reverse direction
112
+ pricingRules: modifiedRules,
113
+ metadata: {
114
+ ...original.metadata,
115
+ counterTo: contractId,
116
+ },
117
+ });
118
+ }
119
+
120
+ /**
121
+ * Terminate a contract.
122
+ */
123
+ terminate(contractId: string): Contract {
124
+ const contract = this.get(contractId);
125
+ contract.status = 'TERMINATED';
126
+ return contract;
127
+ }
128
+
129
+ /**
130
+ * List contracts for an agent.
131
+ */
132
+ listByAgent(agentId: string, status?: ContractStatus): Contract[] {
133
+ return Array.from(this.contracts.values()).filter(c => {
134
+ const partyMatch =
135
+ c.parties.sourceAgentId === agentId ||
136
+ c.parties.targetAgentId === agentId;
137
+ if (!partyMatch) return false;
138
+ if (status && c.status !== status) return false;
139
+ return true;
140
+ });
141
+ }
142
+ }
@@ -0,0 +1,2 @@
1
+ export { ContractManager } from './contract-manager';
2
+ export { MeteringEngine } from './metering';
@@ -0,0 +1,106 @@
1
+ import type { UsageEvent, OutcomeAttestation } from '../types';
2
+ import { generateId, hrTimestamp } from '../utils/id';
3
+
4
+ /**
5
+ * MeteringEngine — Layer 2 primitive
6
+ *
7
+ * Captures usage events with cryptographic integrity.
8
+ * Supports idempotent event submission, deduplication, and
9
+ * outcome attestation for result-based billing.
10
+ */
11
+ export class MeteringEngine {
12
+ private events: Map<string, UsageEvent> = new Map();
13
+ private seenIds: Set<string> = new Set(); // idempotency guard
14
+
15
+ /**
16
+ * Record a usage event.
17
+ * Idempotent: resubmitting the same eventId is a no-op.
18
+ */
19
+ record(event: Omit<UsageEvent, 'eventId' | 'timestamp'> & { eventId?: string }): UsageEvent {
20
+ const eventId = event.eventId || generateId('evt');
21
+
22
+ // Idempotency: skip if already recorded
23
+ if (this.seenIds.has(eventId)) {
24
+ return this.events.get(eventId)!;
25
+ }
26
+
27
+ const fullEvent: UsageEvent = {
28
+ ...event,
29
+ eventId,
30
+ timestamp: hrTimestamp(),
31
+ };
32
+
33
+ this.events.set(eventId, fullEvent);
34
+ this.seenIds.add(eventId);
35
+ return fullEvent;
36
+ }
37
+
38
+ /**
39
+ * Record a batch of usage events.
40
+ */
41
+ recordBatch(events: Array<Omit<UsageEvent, 'eventId' | 'timestamp'>>): UsageEvent[] {
42
+ return events.map(e => this.record(e));
43
+ }
44
+
45
+ /**
46
+ * Create an outcome attestation for result-based billing.
47
+ */
48
+ attestOutcome(data: {
49
+ outcomeType: OutcomeAttestation['outcomeType'];
50
+ claimedBy: string;
51
+ evidence: OutcomeAttestation['evidence'];
52
+ verificationMethod: OutcomeAttestation['verificationMethod'];
53
+ confidenceScore: number;
54
+ disputeWindow?: number;
55
+ }): OutcomeAttestation {
56
+ return {
57
+ outcomeId: generateId('out'),
58
+ outcomeType: data.outcomeType,
59
+ claimedBy: data.claimedBy,
60
+ evidence: data.evidence,
61
+ verificationMethod: data.verificationMethod,
62
+ verifiedBy: undefined,
63
+ confidenceScore: data.confidenceScore,
64
+ attestationStatus: 'CLAIMED',
65
+ disputeWindow: data.disputeWindow || 24 * 60 * 60 * 1000, // 24h default
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Get events for a contract within a time range.
71
+ */
72
+ getByContract(contractId: string, from?: Date, to?: Date): UsageEvent[] {
73
+ let results = Array.from(this.events.values())
74
+ .filter(e => e.contractId === contractId);
75
+
76
+ if (from) results = results.filter(e => e.timestamp >= from);
77
+ if (to) results = results.filter(e => e.timestamp <= to);
78
+
79
+ return results.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
80
+ }
81
+
82
+ /**
83
+ * Get events for a specific agent (as source or target).
84
+ */
85
+ getByAgent(agentId: string, limit = 100): UsageEvent[] {
86
+ return Array.from(this.events.values())
87
+ .filter(e => e.sourceAgentId === agentId || e.targetAgentId === agentId)
88
+ .slice(-limit);
89
+ }
90
+
91
+ /**
92
+ * Get a specific event by ID.
93
+ */
94
+ get(eventId: string): UsageEvent | undefined {
95
+ return this.events.get(eventId);
96
+ }
97
+
98
+ /**
99
+ * Get total metered quantity for a contract.
100
+ */
101
+ totalQuantity(contractId: string): number {
102
+ return Array.from(this.events.values())
103
+ .filter(e => e.contractId === contractId)
104
+ .reduce((sum, e) => sum + e.quantity, 0);
105
+ }
106
+ }