@relaycore/sdk 1.0.0

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,886 @@
1
+ /**
2
+ * Relay Core - Service SDK
3
+ *
4
+ * For service providers to expose services, handle payments, prove delivery,
5
+ * and track reputation on the Relay Core platform.
6
+ *
7
+ * Design Principles:
8
+ * - Explicit service definition (first-class metadata)
9
+ * - Payment-first thinking (not hidden magic)
10
+ * - Delivery proof is sacred
11
+ * - Built-in observability
12
+ * - Runtime-agnostic
13
+ *
14
+ * @example Quickstart (10 minutes)
15
+ * ```ts
16
+ * const service = defineService({
17
+ * name: "price-feed",
18
+ * category: "data.prices",
19
+ * price: "0.01",
20
+ * inputSchema: { type: "object", properties: { pair: { type: "string" } } },
21
+ * outputSchema: { type: "object", properties: { price: { type: "number" } } },
22
+ * });
23
+ *
24
+ * const provider = new RelayService({ wallet, network: "cronos-testnet" });
25
+ * await provider.register(service);
26
+ *
27
+ * // In your request handler:
28
+ * provider.onPaymentReceived(async (ctx) => {
29
+ * const result = await getPrice(ctx.input);
30
+ * ctx.deliver({ result, proof: hash(result) });
31
+ * });
32
+ * ```
33
+ */
34
+
35
+ import { ethers } from 'ethers';
36
+
37
+ // ============================================================================
38
+ // TYPES - Clear, descriptive names
39
+ // ============================================================================
40
+
41
+ /** Network configuration */
42
+ export type Network = 'cronos-mainnet' | 'cronos-testnet' | 'cronos-zkevm';
43
+
44
+ /** Service provider configuration */
45
+ export interface ServiceConfig {
46
+ /** Connected wallet for signing */
47
+ wallet: ethers.Signer;
48
+ /** Target network */
49
+ network?: Network;
50
+ /** API endpoint (defaults to production) */
51
+ apiUrl?: string;
52
+ }
53
+
54
+ /** Service definition - first-class metadata */
55
+ export interface ServiceDefinition {
56
+ /** Unique service name */
57
+ name: string;
58
+ /** Human-readable description */
59
+ description?: string;
60
+ /** Service category (e.g., "data.prices", "trading.execution", "ai.inference") */
61
+ category: string;
62
+ /** Price per call in USDC (e.g., "0.01") */
63
+ price: string;
64
+ /** Service endpoint URL */
65
+ endpoint?: string;
66
+ /** Input JSON schema */
67
+ inputSchema?: JsonSchema;
68
+ /** Output JSON schema */
69
+ outputSchema?: JsonSchema;
70
+ /** Input type name for discovery (e.g., "PriceQuery") */
71
+ inputType?: string;
72
+ /** Output type name for discovery (e.g., "PriceData") */
73
+ outputType?: string;
74
+ /** Searchable tags */
75
+ tags?: string[];
76
+ /** Declared capabilities */
77
+ capabilities?: string[];
78
+ /** Version string */
79
+ version?: string;
80
+ }
81
+
82
+ /** JSON Schema type */
83
+ export interface JsonSchema {
84
+ type: string;
85
+ properties?: Record<string, JsonSchema>;
86
+ required?: string[];
87
+ items?: JsonSchema;
88
+ description?: string;
89
+ [key: string]: unknown;
90
+ }
91
+
92
+ /** Registered service (after registration) */
93
+ export interface RegisteredService extends ServiceDefinition {
94
+ id: string;
95
+ ownerAddress: string;
96
+ registeredAt: Date;
97
+ isActive: boolean;
98
+ }
99
+
100
+ /** Payment context passed to handlers */
101
+ export interface PaymentContext<TInput = unknown> {
102
+ /** Unique payment ID */
103
+ paymentId: string;
104
+ /** Transaction hash */
105
+ txHash: string;
106
+ /** Amount paid in USDC */
107
+ amount: string;
108
+ /** Payer's wallet address */
109
+ payerAddress: string;
110
+ /** Parsed input from request */
111
+ input: TInput;
112
+ /** Timestamp of payment */
113
+ timestamp: Date;
114
+ /** Deliver result with proof */
115
+ deliver: <TOutput>(output: DeliveryProof<TOutput>) => Promise<void>;
116
+ /** Report failure with reason */
117
+ fail: (reason: string, retryable?: boolean) => Promise<void>;
118
+ }
119
+
120
+ /** Delivery proof - the heart of the system */
121
+ export interface DeliveryProof<T = unknown> {
122
+ /** The actual result data */
123
+ result: T;
124
+ /** Hash of the result for verification */
125
+ proof?: string;
126
+ /** Additional evidence (receipts, signatures, etc.) */
127
+ evidence?: Record<string, unknown>;
128
+ /** Execution latency in ms */
129
+ latencyMs?: number;
130
+ }
131
+
132
+ /** Payment status */
133
+ export type PaymentStatus = 'pending' | 'received' | 'settled' | 'failed' | 'timeout';
134
+
135
+ /** Payment event */
136
+ export interface PaymentEvent {
137
+ paymentId: string;
138
+ status: PaymentStatus;
139
+ txHash?: string;
140
+ amount?: string;
141
+ payerAddress?: string;
142
+ timestamp: Date;
143
+ error?: string;
144
+ }
145
+
146
+ /** Outcome types for reputation tracking */
147
+ export type OutcomeType = 'delivered' | 'failed' | 'partial' | 'timeout';
148
+
149
+ /** Outcome record */
150
+ export interface OutcomeRecord {
151
+ paymentId: string;
152
+ outcomeType: OutcomeType;
153
+ latencyMs: number;
154
+ proofHash?: string;
155
+ evidence?: Record<string, unknown>;
156
+ timestamp: Date;
157
+ }
158
+
159
+ /** Service metrics */
160
+ export interface ServiceMetrics {
161
+ timestamp: Date;
162
+ reputationScore: number;
163
+ successRate: number;
164
+ avgLatencyMs: number;
165
+ totalCalls: number;
166
+ totalPayments: number;
167
+ totalRevenue: string;
168
+ }
169
+
170
+ /** Provider reputation */
171
+ export interface ProviderReputation {
172
+ reputationScore: number;
173
+ successRate: number;
174
+ totalDeliveries: number;
175
+ avgLatencyMs: number;
176
+ trend: 'improving' | 'stable' | 'declining';
177
+ rank?: number;
178
+ percentile?: number;
179
+ }
180
+
181
+ /** x402 payment requirements (for 402 responses) */
182
+ export interface PaymentRequirements {
183
+ x402Version: number;
184
+ paymentRequirements: {
185
+ scheme: 'exact';
186
+ network: string;
187
+ payTo: string;
188
+ asset: string;
189
+ maxAmountRequired: string;
190
+ maxTimeoutSeconds: number;
191
+ resource?: string;
192
+ description?: string;
193
+ };
194
+ }
195
+
196
+ /** Observability/logging interface */
197
+ export interface ServiceLogger {
198
+ info(message: string, data?: Record<string, unknown>): void;
199
+ warn(message: string, data?: Record<string, unknown>): void;
200
+ error(message: string, error?: Error, data?: Record<string, unknown>): void;
201
+ metric(name: string, value: number, tags?: Record<string, string>): void;
202
+ }
203
+
204
+ // ============================================================================
205
+ // HELPER FUNCTIONS
206
+ // ============================================================================
207
+
208
+ /**
209
+ * Define a service with typed schema
210
+ *
211
+ * @example
212
+ * const myService = defineService({
213
+ * name: "price-feed",
214
+ * category: "data.prices",
215
+ * price: "0.01",
216
+ * inputSchema: { type: "object", properties: { pair: { type: "string" } } },
217
+ * outputSchema: { type: "object", properties: { price: { type: "number" } } },
218
+ * });
219
+ */
220
+ export function defineService(definition: ServiceDefinition): ServiceDefinition {
221
+ // Validate required fields
222
+ if (!definition.name) throw new Error('Service name is required');
223
+ if (!definition.category) throw new Error('Service category is required');
224
+ if (!definition.price) throw new Error('Service price is required');
225
+
226
+ // Normalize price format
227
+ const normalizedPrice = definition.price.replace('$', '').replace(' USDC', '');
228
+
229
+ return {
230
+ ...definition,
231
+ price: normalizedPrice,
232
+ tags: definition.tags || [],
233
+ capabilities: definition.capabilities || [],
234
+ version: definition.version || '1.0.0',
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Create a hash of data for delivery proof
240
+ *
241
+ * @example
242
+ * const proof = hashProof(result);
243
+ * ctx.deliver({ result, proof });
244
+ */
245
+ export function hashProof(data: unknown): string {
246
+ const json = JSON.stringify(data);
247
+ // Simple hash for demo - in production use crypto.subtle or ethers.keccak256
248
+ let hash = 0;
249
+ for (let i = 0; i < json.length; i++) {
250
+ const char = json.charCodeAt(i);
251
+ hash = ((hash << 5) - hash) + char;
252
+ hash = hash & hash;
253
+ }
254
+ return `0x${Math.abs(hash).toString(16).padStart(16, '0')}`;
255
+ }
256
+
257
+ // ============================================================================
258
+ // IMPLEMENTATION
259
+ // ============================================================================
260
+
261
+ const NETWORK_CONFIG: Record<Network, { apiUrl: string; chainId: number; asset: string }> = {
262
+ 'cronos-mainnet': {
263
+ apiUrl: 'https://api.relaycore.xyz',
264
+ chainId: 25,
265
+ asset: '0xf951eC28187D9E5Ca673Da8FE6757E6f0Be5F77C', // USDC.e
266
+ },
267
+ 'cronos-testnet': {
268
+ apiUrl: 'https://testnet-api.relaycore.xyz',
269
+ chainId: 338,
270
+ asset: '0xf951eC28187D9E5Ca673Da8FE6757E6f0Be5F77C',
271
+ },
272
+ 'cronos-zkevm': {
273
+ apiUrl: 'https://zkevm-api.relaycore.xyz',
274
+ chainId: 388,
275
+ asset: '0xf951eC28187D9E5Ca673Da8FE6757E6f0Be5F77C',
276
+ },
277
+ };
278
+
279
+ /**
280
+ * Relay Service SDK
281
+ *
282
+ * The main entry point for service providers on Relay Core.
283
+ */
284
+ export class RelayService {
285
+ private signer: ethers.Signer;
286
+ private address: string = '';
287
+ private network: Network;
288
+ private apiUrl: string;
289
+ private registeredServices: Map<string, RegisteredService> = new Map();
290
+ private outcomes: OutcomeRecord[] = [];
291
+ private logger: ServiceLogger;
292
+
293
+ // Event handlers
294
+ private paymentReceivedHandlers: Array<(ctx: PaymentContext) => Promise<void>> = [];
295
+ private paymentTimeoutHandlers: Array<(event: PaymentEvent) => Promise<void>> = [];
296
+ private paymentFailedHandlers: Array<(event: PaymentEvent) => Promise<void>> = [];
297
+
298
+ constructor(config: ServiceConfig) {
299
+ this.signer = config.wallet;
300
+ this.network = config.network || 'cronos-mainnet';
301
+ this.apiUrl = config.apiUrl || NETWORK_CONFIG[this.network].apiUrl;
302
+
303
+ // Get address
304
+ config.wallet.getAddress().then(addr => {
305
+ this.address = addr.toLowerCase();
306
+ });
307
+
308
+ // Default console logger
309
+ this.logger = {
310
+ info: (msg, data) => console.log(`[RelayService] ${msg}`, data || ''),
311
+ warn: (msg, data) => console.warn(`[RelayService] ${msg}`, data || ''),
312
+ error: (msg, err, data) => console.error(`[RelayService] ${msg}`, err, data || ''),
313
+ metric: (name, value, tags) => console.log(`[Metric] ${name}=${value}`, tags || ''),
314
+ };
315
+ }
316
+
317
+ // ==========================================================================
318
+ // CONFIGURATION
319
+ // ==========================================================================
320
+
321
+ /**
322
+ * Set custom logger for observability
323
+ */
324
+ setLogger(logger: ServiceLogger): void {
325
+ this.logger = logger;
326
+ }
327
+
328
+ /**
329
+ * Get provider wallet address
330
+ */
331
+ async getAddress(): Promise<string> {
332
+ if (!this.address) {
333
+ this.address = (await this.signer.getAddress()).toLowerCase();
334
+ }
335
+ return this.address;
336
+ }
337
+
338
+ // ==========================================================================
339
+ // SERVICE REGISTRATION - Explicit, first-class
340
+ // ==========================================================================
341
+
342
+ /**
343
+ * Register a service on Relay Core
344
+ *
345
+ * @example
346
+ * const registered = await provider.register(defineService({
347
+ * name: "price-feed",
348
+ * category: "data.prices",
349
+ * price: "0.01",
350
+ * }));
351
+ *
352
+ * console.log(`Service ID: ${registered.id}`);
353
+ */
354
+ async register(service: ServiceDefinition): Promise<RegisteredService> {
355
+ const ownerAddress = await this.getAddress();
356
+
357
+ this.logger.info('Registering service', { name: service.name, category: service.category });
358
+
359
+ const response = await fetch(`${this.apiUrl}/api/services`, {
360
+ method: 'POST',
361
+ headers: { 'Content-Type': 'application/json' },
362
+ body: JSON.stringify({
363
+ name: service.name,
364
+ description: service.description || `${service.name} service`,
365
+ category: service.category,
366
+ endpointUrl: service.endpoint,
367
+ pricePerCall: service.price,
368
+ ownerAddress,
369
+ inputSchema: service.inputSchema,
370
+ outputSchema: service.outputSchema,
371
+ inputType: service.inputType,
372
+ outputType: service.outputType,
373
+ tags: service.tags,
374
+ capabilities: service.capabilities,
375
+ }),
376
+ });
377
+
378
+ if (!response.ok) {
379
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
380
+ this.logger.error('Registration failed', new Error(error.error || 'Unknown'));
381
+ throw new Error(`Failed to register service: ${error.error || response.statusText}`);
382
+ }
383
+
384
+ const data = await response.json();
385
+
386
+ const registered: RegisteredService = {
387
+ ...service,
388
+ id: data.id,
389
+ ownerAddress,
390
+ registeredAt: new Date(),
391
+ isActive: true,
392
+ };
393
+
394
+ this.registeredServices.set(data.id, registered);
395
+ this.logger.info('Service registered', { id: data.id, name: service.name });
396
+
397
+ return registered;
398
+ }
399
+
400
+ /**
401
+ * Update an existing service
402
+ */
403
+ async update(serviceId: string, updates: Partial<ServiceDefinition>): Promise<void> {
404
+ this.logger.info('Updating service', { id: serviceId, updates });
405
+
406
+ const response = await fetch(`${this.apiUrl}/api/services/${serviceId}`, {
407
+ method: 'PUT',
408
+ headers: { 'Content-Type': 'application/json' },
409
+ body: JSON.stringify({
410
+ name: updates.name,
411
+ description: updates.description,
412
+ category: updates.category,
413
+ endpointUrl: updates.endpoint,
414
+ pricePerCall: updates.price,
415
+ inputSchema: updates.inputSchema,
416
+ outputSchema: updates.outputSchema,
417
+ inputType: updates.inputType,
418
+ outputType: updates.outputType,
419
+ tags: updates.tags,
420
+ capabilities: updates.capabilities,
421
+ }),
422
+ });
423
+
424
+ if (!response.ok) {
425
+ throw new Error(`Failed to update service: ${response.statusText}`);
426
+ }
427
+
428
+ // Update local cache
429
+ const existing = this.registeredServices.get(serviceId);
430
+ if (existing) {
431
+ this.registeredServices.set(serviceId, { ...existing, ...updates });
432
+ }
433
+
434
+ this.logger.info('Service updated', { id: serviceId });
435
+ }
436
+
437
+ /**
438
+ * Deactivate a service
439
+ */
440
+ async deactivate(serviceId: string): Promise<void> {
441
+ await this.update(serviceId, { endpoint: undefined } as never);
442
+
443
+ const existing = this.registeredServices.get(serviceId);
444
+ if (existing) {
445
+ existing.isActive = false;
446
+ }
447
+
448
+ this.logger.info('Service deactivated', { id: serviceId });
449
+ }
450
+
451
+ /**
452
+ * Get all registered services for this provider
453
+ */
454
+ async getMyServices(): Promise<RegisteredService[]> {
455
+ const ownerAddress = await this.getAddress();
456
+
457
+ const response = await fetch(
458
+ `${this.apiUrl}/api/services?ownerAddress=${ownerAddress}`
459
+ );
460
+
461
+ if (!response.ok) {
462
+ throw new Error('Failed to fetch services');
463
+ }
464
+
465
+ const data = await response.json();
466
+ return (data.services || []).map((s: Record<string, unknown>) => ({
467
+ id: s.id as string,
468
+ name: s.name as string,
469
+ description: s.description as string,
470
+ category: s.category as string,
471
+ price: s.pricePerCall as string,
472
+ endpoint: s.endpointUrl as string,
473
+ ownerAddress: s.ownerAddress as string,
474
+ registeredAt: new Date(s.createdAt as string),
475
+ isActive: s.isActive as boolean ?? true,
476
+ }));
477
+ }
478
+
479
+ // ==========================================================================
480
+ // PAYMENT HANDLING - Explicit, not magic
481
+ // ==========================================================================
482
+
483
+ /**
484
+ * Generate x402 payment requirements for a 402 response
485
+ *
486
+ * Use this when building your service's payment-required response.
487
+ *
488
+ * @example Express.js middleware
489
+ * ```ts
490
+ * app.use('/api/price', (req, res, next) => {
491
+ * const paymentId = req.headers['x-payment-id'];
492
+ *
493
+ * if (!paymentId) {
494
+ * const requirements = provider.createPaymentRequired({
495
+ * amount: "0.01",
496
+ * resource: "/api/price",
497
+ * description: "Price feed access",
498
+ * });
499
+ * return res.status(402).json(requirements);
500
+ * }
501
+ *
502
+ * next();
503
+ * });
504
+ * ```
505
+ */
506
+ async createPaymentRequired(params: {
507
+ amount: string;
508
+ resource?: string;
509
+ description?: string;
510
+ timeoutSeconds?: number;
511
+ }): Promise<PaymentRequirements> {
512
+ const payTo = await this.getAddress();
513
+ const config = NETWORK_CONFIG[this.network];
514
+
515
+ return {
516
+ x402Version: 1,
517
+ paymentRequirements: {
518
+ scheme: 'exact',
519
+ network: this.network === 'cronos-mainnet' ? 'cronos-mainnet' : 'cronos-testnet',
520
+ payTo,
521
+ asset: config.asset,
522
+ maxAmountRequired: params.amount,
523
+ maxTimeoutSeconds: params.timeoutSeconds || 60,
524
+ resource: params.resource,
525
+ description: params.description,
526
+ },
527
+ };
528
+ }
529
+
530
+ /**
531
+ * Verify a payment was made
532
+ *
533
+ * @example
534
+ * const { verified, amount, payerAddress } = await provider.verifyPayment(paymentId);
535
+ * if (!verified) {
536
+ * return res.status(402).json({ error: 'Payment not verified' });
537
+ * }
538
+ */
539
+ async verifyPayment(paymentId: string): Promise<{
540
+ verified: boolean;
541
+ status: PaymentStatus;
542
+ amount?: string;
543
+ payerAddress?: string;
544
+ txHash?: string;
545
+ }> {
546
+ const response = await fetch(`${this.apiUrl}/api/payments/${paymentId}`);
547
+
548
+ if (!response.ok) {
549
+ return { verified: false, status: 'failed' };
550
+ }
551
+
552
+ const data = await response.json();
553
+ const payment = data.payment;
554
+
555
+ return {
556
+ verified: payment?.status === 'settled',
557
+ status: payment?.status || 'pending',
558
+ amount: payment?.amount,
559
+ payerAddress: payment?.payerAddress,
560
+ txHash: payment?.txHash,
561
+ };
562
+ }
563
+
564
+ /**
565
+ * Register handler for payment received events
566
+ *
567
+ * @example
568
+ * provider.onPaymentReceived(async (ctx) => {
569
+ * const result = await processRequest(ctx.input);
570
+ * ctx.deliver({
571
+ * result,
572
+ * proof: hashProof(result),
573
+ * latencyMs: Date.now() - ctx.timestamp.getTime(),
574
+ * });
575
+ * });
576
+ */
577
+ onPaymentReceived(handler: (ctx: PaymentContext) => Promise<void>): () => void {
578
+ this.paymentReceivedHandlers.push(handler);
579
+ return () => {
580
+ const idx = this.paymentReceivedHandlers.indexOf(handler);
581
+ if (idx > -1) this.paymentReceivedHandlers.splice(idx, 1);
582
+ };
583
+ }
584
+
585
+ /**
586
+ * Register handler for payment timeout events
587
+ */
588
+ onPaymentTimeout(handler: (event: PaymentEvent) => Promise<void>): () => void {
589
+ this.paymentTimeoutHandlers.push(handler);
590
+ return () => {
591
+ const idx = this.paymentTimeoutHandlers.indexOf(handler);
592
+ if (idx > -1) this.paymentTimeoutHandlers.splice(idx, 1);
593
+ };
594
+ }
595
+
596
+ /**
597
+ * Register handler for payment failed events
598
+ */
599
+ onPaymentFailed(handler: (event: PaymentEvent) => Promise<void>): () => void {
600
+ this.paymentFailedHandlers.push(handler);
601
+ return () => {
602
+ const idx = this.paymentFailedHandlers.indexOf(handler);
603
+ if (idx > -1) this.paymentFailedHandlers.splice(idx, 1);
604
+ };
605
+ }
606
+
607
+ /**
608
+ * Process a verified payment and trigger handlers
609
+ *
610
+ * Call this from your request handler after verifying payment.
611
+ */
612
+ async processPayment<TInput = unknown>(params: {
613
+ paymentId: string;
614
+ txHash: string;
615
+ amount: string;
616
+ payerAddress: string;
617
+ input: TInput;
618
+ }): Promise<void> {
619
+ const ctx: PaymentContext<TInput> = {
620
+ paymentId: params.paymentId,
621
+ txHash: params.txHash,
622
+ amount: params.amount,
623
+ payerAddress: params.payerAddress,
624
+ input: params.input,
625
+ timestamp: new Date(),
626
+ deliver: async (output) => this.recordDelivery(params.paymentId, output),
627
+ fail: async (reason, retryable) => this.recordFailure(params.paymentId, reason, retryable),
628
+ };
629
+
630
+ this.logger.info('Processing payment', { paymentId: params.paymentId, amount: params.amount });
631
+
632
+ for (const handler of this.paymentReceivedHandlers) {
633
+ try {
634
+ await handler(ctx as PaymentContext);
635
+ } catch (error) {
636
+ this.logger.error('Payment handler error', error instanceof Error ? error : new Error(String(error)));
637
+ await ctx.fail(error instanceof Error ? error.message : 'Handler error', true);
638
+ }
639
+ }
640
+ }
641
+
642
+ // ==========================================================================
643
+ // DELIVERY PROOF - Sacred
644
+ // ==========================================================================
645
+
646
+ /**
647
+ * Record a successful delivery
648
+ *
649
+ * Called automatically by ctx.deliver() or can be called directly.
650
+ */
651
+ async recordDelivery<T>(paymentId: string, output: DeliveryProof<T>): Promise<void> {
652
+ const outcome: OutcomeRecord = {
653
+ paymentId,
654
+ outcomeType: 'delivered',
655
+ latencyMs: output.latencyMs || 0,
656
+ proofHash: output.proof,
657
+ evidence: output.evidence,
658
+ timestamp: new Date(),
659
+ };
660
+
661
+ this.outcomes.push(outcome);
662
+ this.logger.info('Delivery recorded', { paymentId, proof: output.proof });
663
+ this.logger.metric('delivery.success', 1, { paymentId });
664
+ this.logger.metric('delivery.latency', output.latencyMs || 0, { paymentId });
665
+
666
+ // Report to API
667
+ await this.reportOutcome(outcome);
668
+ }
669
+
670
+ /**
671
+ * Record a failure
672
+ *
673
+ * Called automatically by ctx.fail() or can be called directly.
674
+ */
675
+ async recordFailure(paymentId: string, reason: string, retryable?: boolean): Promise<void> {
676
+ const outcome: OutcomeRecord = {
677
+ paymentId,
678
+ outcomeType: retryable ? 'partial' : 'failed',
679
+ latencyMs: 0,
680
+ evidence: { reason, retryable },
681
+ timestamp: new Date(),
682
+ };
683
+
684
+ this.outcomes.push(outcome);
685
+ this.logger.warn('Failure recorded', { paymentId, reason, retryable });
686
+ this.logger.metric('delivery.failure', 1, { paymentId, reason });
687
+
688
+ await this.reportOutcome(outcome);
689
+ }
690
+
691
+ private async reportOutcome(outcome: OutcomeRecord): Promise<void> {
692
+ try {
693
+ await fetch(`${this.apiUrl}/api/outcomes`, {
694
+ method: 'POST',
695
+ headers: { 'Content-Type': 'application/json' },
696
+ body: JSON.stringify({
697
+ paymentId: outcome.paymentId,
698
+ outcomeType: outcome.outcomeType,
699
+ latencyMs: outcome.latencyMs,
700
+ proofHash: outcome.proofHash,
701
+ evidence: outcome.evidence,
702
+ }),
703
+ });
704
+ } catch (error) {
705
+ this.logger.error('Failed to report outcome', error instanceof Error ? error : new Error(String(error)));
706
+ }
707
+ }
708
+
709
+ // ==========================================================================
710
+ // OBSERVABILITY - Built-in, not afterthought
711
+ // ==========================================================================
712
+
713
+ /**
714
+ * Get current reputation
715
+ */
716
+ async getReputation(): Promise<ProviderReputation> {
717
+ const ownerAddress = await this.getAddress();
718
+
719
+ const response = await fetch(
720
+ `${this.apiUrl}/api/services?ownerAddress=${ownerAddress}&limit=1`
721
+ );
722
+
723
+ if (!response.ok) {
724
+ throw new Error('Failed to fetch reputation');
725
+ }
726
+
727
+ const data = await response.json();
728
+ const service = data.services?.[0];
729
+
730
+ if (!service) {
731
+ return {
732
+ reputationScore: 0,
733
+ successRate: 0,
734
+ totalDeliveries: 0,
735
+ avgLatencyMs: 0,
736
+ trend: 'stable',
737
+ };
738
+ }
739
+
740
+ return {
741
+ reputationScore: service.reputationScore || 0,
742
+ successRate: service.successRate || 0,
743
+ totalDeliveries: service.totalPayments || 0,
744
+ avgLatencyMs: service.avgLatencyMs || 0,
745
+ trend: service.trend || 'stable',
746
+ rank: service.rank,
747
+ percentile: service.percentile,
748
+ };
749
+ }
750
+
751
+ /**
752
+ * Get service metrics history
753
+ */
754
+ async getMetrics(serviceId: string, options: {
755
+ from?: Date;
756
+ to?: Date;
757
+ interval?: '1h' | '1d' | '7d';
758
+ } = {}): Promise<ServiceMetrics[]> {
759
+ const params = new URLSearchParams();
760
+ if (options.from) params.set('from', options.from.toISOString());
761
+ if (options.to) params.set('to', options.to.toISOString());
762
+ if (options.interval) params.set('interval', options.interval);
763
+
764
+ const response = await fetch(
765
+ `${this.apiUrl}/api/services/${serviceId}/metrics?${params}`
766
+ );
767
+
768
+ if (!response.ok) {
769
+ throw new Error('Failed to fetch metrics');
770
+ }
771
+
772
+ const data = await response.json();
773
+ return (data.data || []).map((m: Record<string, unknown>) => ({
774
+ timestamp: new Date(m.timestamp as string),
775
+ reputationScore: m.reputationScore as number || 0,
776
+ successRate: m.successRate as number || 0,
777
+ avgLatencyMs: m.avgLatencyMs as number || 0,
778
+ totalCalls: m.totalCalls as number || 0,
779
+ totalPayments: m.totalPayments as number || 0,
780
+ totalRevenue: m.totalRevenue as string || '0',
781
+ }));
782
+ }
783
+
784
+ /**
785
+ * Get local outcome stats (in-memory)
786
+ */
787
+ getLocalStats(): {
788
+ totalOutcomes: number;
789
+ deliveries: number;
790
+ failures: number;
791
+ successRate: number;
792
+ avgLatencyMs: number;
793
+ } {
794
+ const deliveries = this.outcomes.filter(o => o.outcomeType === 'delivered').length;
795
+ const failures = this.outcomes.filter(o => o.outcomeType === 'failed').length;
796
+ const total = this.outcomes.length;
797
+ const avgLatency = total > 0
798
+ ? this.outcomes.reduce((sum, o) => sum + o.latencyMs, 0) / total
799
+ : 0;
800
+
801
+ return {
802
+ totalOutcomes: total,
803
+ deliveries,
804
+ failures,
805
+ successRate: total > 0 ? deliveries / total : 0,
806
+ avgLatencyMs: Math.round(avgLatency),
807
+ };
808
+ }
809
+
810
+ /**
811
+ * Get recent outcomes
812
+ */
813
+ getRecentOutcomes(limit: number = 10): OutcomeRecord[] {
814
+ return this.outcomes.slice(-limit);
815
+ }
816
+ }
817
+
818
+ // ============================================================================
819
+ // EXPRESS MIDDLEWARE
820
+ // ============================================================================
821
+
822
+ /**
823
+ * Create Express middleware for x402 payment handling
824
+ *
825
+ * @example
826
+ * const paymentMiddleware = createPaymentMiddleware(provider, {
827
+ * amount: "0.01",
828
+ * description: "API access",
829
+ * });
830
+ *
831
+ * app.use('/api/protected', paymentMiddleware, (req, res) => {
832
+ * res.json({ data: 'protected data' });
833
+ * });
834
+ */
835
+ export function createPaymentMiddleware(
836
+ provider: RelayService,
837
+ options: {
838
+ amount: string;
839
+ description?: string;
840
+ timeoutSeconds?: number;
841
+ }
842
+ ) {
843
+ return async (req: { headers: Record<string, string | undefined> }, res: {
844
+ status: (code: number) => { json: (data: unknown) => void };
845
+ }, next: () => void) => {
846
+ const paymentId = req.headers['x-payment-id'];
847
+ const paymentTx = req.headers['x-payment'];
848
+
849
+ if (!paymentId || !paymentTx) {
850
+ const requirements = await provider.createPaymentRequired({
851
+ amount: options.amount,
852
+ description: options.description,
853
+ timeoutSeconds: options.timeoutSeconds,
854
+ });
855
+ return res.status(402).json(requirements);
856
+ }
857
+
858
+ // Verify payment
859
+ const verification = await provider.verifyPayment(paymentId);
860
+
861
+ if (!verification.verified) {
862
+ return res.status(402).json({
863
+ error: 'Payment not verified',
864
+ status: verification.status,
865
+ });
866
+ }
867
+
868
+ next();
869
+ };
870
+ }
871
+
872
+ // ============================================================================
873
+ // FACTORY & EXPORTS
874
+ // ============================================================================
875
+
876
+ /**
877
+ * Create a Relay Service instance
878
+ *
879
+ * @example
880
+ * const provider = createService({ wallet, network: "cronos-testnet" });
881
+ */
882
+ export function createService(config: ServiceConfig): RelayService {
883
+ return new RelayService(config);
884
+ }
885
+
886
+ export default RelayService;