@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.
package/relay-agent.ts ADDED
@@ -0,0 +1,1414 @@
1
+ /**
2
+ * Relay Core - Agent SDK
3
+ *
4
+ * For AI agents to discover services, make decisions, and execute workflows.
5
+ *
6
+ * Design Principles:
7
+ * - Decision abstraction, not CRUD
8
+ * - Workflow primitives first
9
+ * - Failure is first-class
10
+ * - 10-minute quickstart
11
+ *
12
+ * @example Quickstart
13
+ * ```ts
14
+ * const agent = new RelayAgent({ wallet, network: "cronos-testnet" });
15
+ *
16
+ * // Select best service by policy
17
+ * const service = await agent.selectService({
18
+ * category: "data.prices",
19
+ * constraints: { minReputation: 90 }
20
+ * });
21
+ *
22
+ * // Execute with automatic payment
23
+ * const result = await agent.execute(service, { pair: "BTC/USD" });
24
+ * ```
25
+ */
26
+
27
+ import { ethers } from 'ethers';
28
+
29
+ // ============================================================================
30
+ // TYPES - Clear, descriptive names
31
+ // ============================================================================
32
+
33
+ /** Network configuration */
34
+ export type Network = 'cronos-mainnet' | 'cronos-testnet' | 'cronos-zkevm';
35
+
36
+ /** Agent configuration - minimal required, progressive optional */
37
+ export interface AgentConfig {
38
+ /** Connected wallet (ethers.Signer or address for read-only) */
39
+ wallet: ethers.Signer | string;
40
+ /** Relay Core API Key (starts with rc_...) */
41
+ apiKey: string;
42
+ /** Target network */
43
+ network?: Network;
44
+ /** API endpoint (defaults to production) */
45
+ apiUrl?: string;
46
+ }
47
+
48
+ /** Trust policy for service selection */
49
+ export interface TrustPolicy {
50
+ /** Minimum reputation score (0-100) */
51
+ minReputation?: number;
52
+ /** Maximum acceptable latency in ms */
53
+ maxLatency?: number;
54
+ /** Maximum price per call in USDC */
55
+ maxPrice?: number;
56
+ /** Require verified/reliable services only */
57
+ verifiedOnly?: boolean;
58
+ /** Preferred service providers (addresses) */
59
+ preferredProviders?: string[];
60
+ /** Blacklisted providers (addresses) */
61
+ blacklistedProviders?: string[];
62
+ }
63
+
64
+ /** Service selection criteria */
65
+ export interface ServiceCriteria {
66
+ /** Service category (e.g., "data.prices", "trading.execution") */
67
+ category?: string;
68
+ /** Required input type */
69
+ inputType?: string;
70
+ /** Required output type */
71
+ outputType?: string;
72
+ /** Required tags */
73
+ tags?: string[];
74
+ /** Required capabilities */
75
+ capabilities?: string[];
76
+ /** Trust constraints */
77
+ constraints?: TrustPolicy;
78
+ }
79
+
80
+ /** Selected service with selection explanation */
81
+ export interface SelectedService {
82
+ id: string;
83
+ name: string;
84
+ endpoint: string;
85
+ price: string;
86
+ provider: string;
87
+ reputation: number;
88
+ latency: number;
89
+ /** Why this service was selected */
90
+ selectionReason: string;
91
+ /** Score breakdown */
92
+ scoreBreakdown: {
93
+ reputation: number;
94
+ latency: number;
95
+ price: number;
96
+ total: number;
97
+ };
98
+ }
99
+
100
+ /** Execution result with full context */
101
+ export interface ExecutionResult<T = unknown> {
102
+ success: boolean;
103
+ data?: T;
104
+ error?: ExecutionError;
105
+ /** Payment details if payment was made */
106
+ payment?: {
107
+ id: string;
108
+ txHash: string;
109
+ amount: string;
110
+ };
111
+ /** Performance metrics */
112
+ metrics: {
113
+ totalMs: number;
114
+ paymentMs?: number;
115
+ serviceMs?: number;
116
+ };
117
+ }
118
+
119
+ /** Structured error with actionable information */
120
+ export interface ExecutionError {
121
+ code: ErrorCode;
122
+ message: string;
123
+ /** Can this operation be retried? */
124
+ retryable: boolean;
125
+ /** If retryable, suggested wait time in ms */
126
+ retryAfterMs?: number;
127
+ /** Original error details */
128
+ details?: unknown;
129
+ }
130
+
131
+ /** Error codes for programmatic handling */
132
+ export type ErrorCode =
133
+ | 'SERVICE_NOT_FOUND'
134
+ | 'SERVICE_UNAVAILABLE'
135
+ | 'PAYMENT_FAILED'
136
+ | 'PAYMENT_TIMEOUT'
137
+ | 'EXECUTION_FAILED'
138
+ | 'EXECUTION_TIMEOUT'
139
+ | 'INSUFFICIENT_BALANCE'
140
+ | 'UNAUTHORIZED'
141
+ | 'RATE_LIMITED'
142
+ | 'INVALID_INPUT'
143
+ | 'PARTIAL_SUCCESS'
144
+ | 'UNKNOWN';
145
+
146
+ /** Workflow step definition */
147
+ export interface WorkflowStep<TInput = unknown, TOutput = unknown> {
148
+ name: string;
149
+ service?: SelectedService;
150
+ serviceId?: string;
151
+ criteria?: ServiceCriteria;
152
+ transform?: (input: TInput) => TOutput | Promise<TOutput>;
153
+ timeout?: number;
154
+ retries?: number;
155
+ fallback?: WorkflowStep<TInput, TOutput>;
156
+ onSuccess?: (result: TOutput) => void | Promise<void>;
157
+ onFailure?: (error: ExecutionError) => void | Promise<void>;
158
+ }
159
+
160
+ /** Workflow execution result */
161
+ export interface WorkflowResult<T = unknown> {
162
+ success: boolean;
163
+ data?: T;
164
+ /** Results from each step */
165
+ stepResults: Array<{
166
+ stepName: string;
167
+ success: boolean;
168
+ data?: unknown;
169
+ error?: ExecutionError;
170
+ durationMs: number;
171
+ }>;
172
+ /** Total workflow duration */
173
+ totalMs: number;
174
+ /** Number of steps completed */
175
+ completedSteps: number;
176
+ /** Number of steps that failed */
177
+ failedSteps: number;
178
+ }
179
+
180
+ /** Outcome record for memory */
181
+ export interface OutcomeRecord {
182
+ timestamp: Date;
183
+ serviceId: string;
184
+ success: boolean;
185
+ latencyMs: number;
186
+ paymentAmount?: string;
187
+ error?: ExecutionError;
188
+ }
189
+
190
+ /** Memory hooks for agent learning */
191
+ export interface AgentMemory {
192
+ record(outcome: OutcomeRecord): void;
193
+ getHistory(serviceId?: string): OutcomeRecord[];
194
+ getStats(): { totalCalls: number; successRate: number; avgLatency: number };
195
+ clear(): void;
196
+ }
197
+
198
+ /** A2A Agent Card Resource */
199
+ export interface AgentCardResource {
200
+ id: string;
201
+ title: string;
202
+ url: string;
203
+ price?: string;
204
+ paywall: {
205
+ protocol: 'x402';
206
+ settlement: string;
207
+ };
208
+ }
209
+
210
+ /** A2A Agent Card for discovery */
211
+ export interface AgentCard {
212
+ name: string;
213
+ description: string;
214
+ url: string;
215
+ version?: string;
216
+ network: string;
217
+ capabilities?: string[];
218
+ resources: AgentCardResource[];
219
+ contracts?: {
220
+ escrowSession?: string;
221
+ identityRegistry?: string;
222
+ reputationRegistry?: string;
223
+ usdcToken?: string;
224
+ };
225
+ x402?: {
226
+ facilitator?: string;
227
+ token?: string;
228
+ chainId?: number;
229
+ };
230
+ }
231
+
232
+ /** Task state enum */
233
+ export type TaskState = 'idle' | 'pending' | 'settled' | 'failed';
234
+
235
+ /** Task Artifact for tracking agent actions */
236
+ export interface TaskArtifact {
237
+ task_id: string;
238
+ agent_id: string;
239
+ service_id?: string;
240
+ session_id?: string;
241
+ state: TaskState;
242
+ payment_id?: string;
243
+ facilitator_tx?: string;
244
+ retries: number;
245
+ timestamps: {
246
+ created: string;
247
+ updated: string;
248
+ completed?: string;
249
+ };
250
+ inputs: Record<string, unknown>;
251
+ outputs: Record<string, unknown>;
252
+ error?: {
253
+ code: string;
254
+ message: string;
255
+ retryable: boolean;
256
+ };
257
+ metrics?: {
258
+ total_ms: number;
259
+ payment_ms?: number;
260
+ service_ms?: number;
261
+ };
262
+ }
263
+
264
+ /** Task statistics summary */
265
+ export interface TaskStats {
266
+ total: number;
267
+ pending: number;
268
+ settled: number;
269
+ failed: number;
270
+ success_rate: number;
271
+ avg_duration_ms: number;
272
+ }
273
+
274
+ // ============================================================================
275
+ // IMPLEMENTATION
276
+ // ============================================================================
277
+
278
+ const NETWORK_CONFIG: Record<Network, { apiUrl: string; chainId: number }> = {
279
+ 'cronos-mainnet': { apiUrl: 'https://api.relaycore.xyz', chainId: 25 },
280
+ 'cronos-testnet': { apiUrl: 'https://testnet-api.relaycore.xyz', chainId: 338 },
281
+ 'cronos-zkevm': { apiUrl: 'https://zkevm-api.relaycore.xyz', chainId: 388 },
282
+ };
283
+
284
+ const FACILITATOR_URL = 'https://facilitator.cronoslabs.org/v2/x402';
285
+
286
+ /**
287
+ * Relay Agent SDK
288
+ *
289
+ * The main entry point for AI agents to interact with Relay Core services.
290
+ */
291
+ /** Internal discovered service type with all fields */
292
+ interface DiscoveredServiceInternal {
293
+ id: string;
294
+ name: string;
295
+ endpoint: string;
296
+ price: string;
297
+ provider: string;
298
+ reputation: number;
299
+ latency: number;
300
+ category: string;
301
+ inputType?: string;
302
+ outputType?: string;
303
+ tags: string[];
304
+ capabilities: string[];
305
+ verified: boolean;
306
+ }
307
+
308
+ export class RelayAgent {
309
+ private signer: ethers.Signer | null = null;
310
+ private _address: string = '';
311
+ private network: Network;
312
+ private apiUrl: string;
313
+ private apiKey: string;
314
+ private trustPolicy: TrustPolicy = {};
315
+ private memoryStore: OutcomeRecord[] = [];
316
+
317
+ constructor(config: AgentConfig) {
318
+ this.network = config.network || 'cronos-mainnet';
319
+ this.apiUrl = config.apiUrl || NETWORK_CONFIG[this.network].apiUrl;
320
+ this.apiKey = config.apiKey;
321
+
322
+ if (typeof config.wallet === 'string') {
323
+ this._address = config.wallet.toLowerCase();
324
+ } else {
325
+ this.signer = config.wallet;
326
+ // Get address async
327
+ config.wallet.getAddress().then(addr => {
328
+ this._address = addr.toLowerCase();
329
+ });
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Get authenticated headers for API requests
335
+ */
336
+ private getHeaders(): HeadersInit {
337
+ return {
338
+ 'Content-Type': 'application/json',
339
+ 'x-api-key': this.apiKey
340
+ };
341
+ }
342
+
343
+ // ==========================================================================
344
+ // CONFIGURATION - Progressive, not overwhelming
345
+ // ==========================================================================
346
+
347
+ /**
348
+ * Get agent's wallet address
349
+ */
350
+ async getAddress(): Promise<string> {
351
+ if (this.signer && !this._address) {
352
+ this._address = (await this.signer.getAddress()).toLowerCase();
353
+ }
354
+ return this._address;
355
+ }
356
+
357
+ /**
358
+ * Set trust policy for service selection
359
+ *
360
+ * @example
361
+ * agent.setTrustPolicy({
362
+ * minReputation: 90,
363
+ * maxLatency: 500,
364
+ * verifiedOnly: true
365
+ * });
366
+ */
367
+ setTrustPolicy(policy: TrustPolicy): void {
368
+ this.trustPolicy = { ...this.trustPolicy, ...policy };
369
+ }
370
+
371
+ /**
372
+ * Get current trust policy
373
+ */
374
+ getTrustPolicy(): TrustPolicy {
375
+ return { ...this.trustPolicy };
376
+ }
377
+
378
+ // ==========================================================================
379
+ // SERVICE SELECTION - Decision abstraction, not CRUD
380
+ // ==========================================================================
381
+
382
+ /**
383
+ * Select the best service matching criteria
384
+ *
385
+ * This is the main decision interface. The SDK handles:
386
+ * - Filtering by criteria
387
+ * - Scoring by trust policy
388
+ * - Explaining why a service was selected
389
+ *
390
+ * @example
391
+ * const service = await agent.selectService({
392
+ * category: "data.prices",
393
+ * constraints: { minReputation: 90, maxLatency: 200 }
394
+ * });
395
+ *
396
+ * console.log(service.selectionReason);
397
+ * // "Highest score (92.5) based on: reputation=95, latency=150ms, price=$0.01"
398
+ */
399
+ async selectService(criteria: ServiceCriteria): Promise<SelectedService | null> {
400
+ const services = await this.discoverServices(criteria);
401
+
402
+ if (services.length === 0) return null;
403
+
404
+ // Apply trust policy + criteria constraints
405
+ const constraints = { ...this.trustPolicy, ...criteria.constraints };
406
+
407
+ const scored = services
408
+ .filter(s => this.passesConstraints(s, constraints))
409
+ .map(s => this.scoreService(s, constraints))
410
+ .sort((a, b) => b.score - a.score);
411
+
412
+ if (scored.length === 0) return null;
413
+
414
+ const best = scored[0];
415
+ return {
416
+ id: best.service.id,
417
+ name: best.service.name,
418
+ endpoint: best.service.endpoint,
419
+ price: best.service.price,
420
+ provider: best.service.provider,
421
+ reputation: best.service.reputation,
422
+ latency: best.service.latency,
423
+ selectionReason: this.explainSelection(best),
424
+ scoreBreakdown: {
425
+ reputation: best.repScore,
426
+ latency: best.latScore,
427
+ price: best.priceScore,
428
+ total: best.score,
429
+ },
430
+ };
431
+ }
432
+
433
+ /**
434
+ * Discover all services matching criteria (raw, unfiltered)
435
+ */
436
+ async discoverServices(criteria: ServiceCriteria): Promise<Array<{
437
+ id: string;
438
+ name: string;
439
+ endpoint: string;
440
+ price: string;
441
+ provider: string;
442
+ reputation: number;
443
+ latency: number;
444
+ category: string;
445
+ inputType?: string;
446
+ outputType?: string;
447
+ tags: string[];
448
+ capabilities: string[];
449
+ verified: boolean;
450
+ }>> {
451
+ const params = new URLSearchParams();
452
+ if (criteria.category) params.set('category', criteria.category);
453
+ if (criteria.inputType) params.set('inputType', criteria.inputType);
454
+ if (criteria.outputType) params.set('outputType', criteria.outputType);
455
+ if (criteria.tags) params.set('tags', criteria.tags.join(','));
456
+ if (criteria.capabilities) params.set('capabilities', criteria.capabilities.join(','));
457
+ params.set('limit', '100');
458
+
459
+ const response = await fetch(`${this.apiUrl}/api/services?${params}`, {
460
+ headers: this.getHeaders()
461
+ });
462
+ if (!response.ok) {
463
+ throw this.createError('SERVICE_UNAVAILABLE', 'Failed to discover services', true);
464
+ }
465
+
466
+ const data = await response.json();
467
+ return (data.services || []).map((s: Record<string, unknown>) => ({
468
+ id: s.id as string,
469
+ name: s.name as string,
470
+ endpoint: s.endpointUrl as string || s.endpoint_url as string || '',
471
+ price: s.pricePerCall as string || s.price_per_call as string || '0',
472
+ provider: s.ownerAddress as string || s.owner_address as string || '',
473
+ reputation: (s.reputationScore as number) || (s.reputation_score as number) || 0,
474
+ latency: (s.avgLatencyMs as number) || (s.avg_latency_ms as number) || 0,
475
+ category: s.category as string || '',
476
+ inputType: (s.schema as Record<string, unknown>)?.inputType as string,
477
+ outputType: (s.schema as Record<string, unknown>)?.outputType as string,
478
+ tags: ((s.schema as Record<string, unknown>)?.tags as string[]) || [],
479
+ capabilities: ((s.schema as Record<string, unknown>)?.capabilities as string[]) || [],
480
+ verified: (s.health as Record<string, unknown>)?.reliable as boolean || false,
481
+ }));
482
+ }
483
+
484
+ // ==========================================================================
485
+ // EXECUTION - Payment-first, failure-aware
486
+ // ==========================================================================
487
+
488
+ /**
489
+ * Execute a service with automatic payment handling
490
+ *
491
+ * @example
492
+ * const result = await agent.execute(service, { pair: "BTC/USD" });
493
+ *
494
+ * if (result.success) {
495
+ * console.log("Price:", result.data.price);
496
+ * } else if (result.error?.retryable) {
497
+ * // Wait and retry
498
+ * await sleep(result.error.retryAfterMs);
499
+ * const retry = await agent.execute(service, input);
500
+ * }
501
+ */
502
+ async execute<TInput = unknown, TOutput = unknown>(
503
+ service: SelectedService | string,
504
+ input?: TInput,
505
+ options: { timeout?: number } = {}
506
+ ): Promise<ExecutionResult<TOutput>> {
507
+ const startTime = performance.now();
508
+ const timeout = options.timeout || 30000;
509
+
510
+ // Resolve service
511
+ const resolvedService = typeof service === 'string'
512
+ ? await this.getServiceById(service)
513
+ : service;
514
+
515
+ if (!resolvedService) {
516
+ return {
517
+ success: false,
518
+ error: this.createError('SERVICE_NOT_FOUND', 'Service not found', false),
519
+ metrics: { totalMs: Math.round(performance.now() - startTime) },
520
+ };
521
+ }
522
+
523
+ // Make initial request
524
+ const controller = new AbortController();
525
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
526
+
527
+ try {
528
+ let response = await fetch(resolvedService.endpoint, {
529
+ method: input ? 'POST' : 'GET',
530
+ headers: this.getHeaders(),
531
+ body: input ? JSON.stringify(input) : undefined,
532
+ signal: controller.signal,
533
+ });
534
+
535
+ let paymentInfo: ExecutionResult<TOutput>['payment'];
536
+ let paymentMs: number | undefined;
537
+
538
+ // Handle 402 Payment Required
539
+ if (response.status === 402) {
540
+ if (!this.signer) {
541
+ clearTimeout(timeoutId);
542
+ return {
543
+ success: false,
544
+ error: this.createError('UNAUTHORIZED', 'Signer required for paid services', false),
545
+ metrics: { totalMs: Math.round(performance.now() - startTime) },
546
+ };
547
+ }
548
+
549
+ const paymentRequired = await response.json();
550
+ const requirements = paymentRequired.paymentRequirements;
551
+
552
+ const paymentStart = performance.now();
553
+ const payment = await this.makePayment(requirements);
554
+ paymentMs = Math.round(performance.now() - paymentStart);
555
+
556
+ paymentInfo = {
557
+ id: payment.paymentId,
558
+ txHash: payment.txHash,
559
+ amount: requirements.maxAmountRequired,
560
+ };
561
+
562
+ // Retry with payment
563
+ response = await fetch(resolvedService.endpoint, {
564
+ method: input ? 'POST' : 'GET',
565
+ headers: {
566
+ ...this.getHeaders(),
567
+ 'X-Payment': payment.txHash,
568
+ 'X-Payment-Id': payment.paymentId,
569
+ },
570
+ body: input ? JSON.stringify(input) : undefined,
571
+ signal: controller.signal,
572
+ });
573
+ }
574
+
575
+ clearTimeout(timeoutId);
576
+ const totalMs = Math.round(performance.now() - startTime);
577
+ const serviceMs = paymentMs ? totalMs - paymentMs : totalMs;
578
+
579
+ if (!response.ok) {
580
+ const error = await response.json().catch(() => ({}));
581
+ return {
582
+ success: false,
583
+ error: this.createError(
584
+ response.status === 429 ? 'RATE_LIMITED' : 'EXECUTION_FAILED',
585
+ error.message || `Service returned ${response.status}`,
586
+ response.status === 429 || response.status >= 500
587
+ ),
588
+ payment: paymentInfo,
589
+ metrics: { totalMs, paymentMs, serviceMs },
590
+ };
591
+ }
592
+
593
+ const data = await response.json();
594
+
595
+ // Record outcome for memory
596
+ this.recordOutcome({
597
+ timestamp: new Date(),
598
+ serviceId: resolvedService.id,
599
+ success: true,
600
+ latencyMs: serviceMs,
601
+ paymentAmount: paymentInfo?.amount,
602
+ });
603
+
604
+ return {
605
+ success: true,
606
+ data,
607
+ payment: paymentInfo,
608
+ metrics: { totalMs, paymentMs, serviceMs },
609
+ };
610
+
611
+ } catch (err) {
612
+ clearTimeout(timeoutId);
613
+ const totalMs = Math.round(performance.now() - startTime);
614
+
615
+ const isTimeout = err instanceof Error && err.name === 'AbortError';
616
+ return {
617
+ success: false,
618
+ error: this.createError(
619
+ isTimeout ? 'EXECUTION_TIMEOUT' : 'EXECUTION_FAILED',
620
+ isTimeout ? 'Request timed out' : (err instanceof Error ? err.message : 'Unknown error'),
621
+ true,
622
+ isTimeout ? 5000 : 1000
623
+ ),
624
+ metrics: { totalMs },
625
+ };
626
+ }
627
+ }
628
+
629
+ // ==========================================================================
630
+ // WORKFLOWS - Multi-step execution with retries and fallbacks
631
+ // ==========================================================================
632
+
633
+ /**
634
+ * Execute a multi-step workflow
635
+ *
636
+ * @example
637
+ * const result = await agent.executeWorkflow([
638
+ * { name: 'getPrice', criteria: { category: 'data.prices' } },
639
+ * { name: 'validate', transform: (price) => price.value > 0 ? price : null },
640
+ * { name: 'trade', criteria: { category: 'trading.execution' } },
641
+ * ], { pair: 'BTC/USD' });
642
+ */
643
+ async executeWorkflow<TInput = unknown, TOutput = unknown>(
644
+ steps: WorkflowStep[],
645
+ initialInput: TInput
646
+ ): Promise<WorkflowResult<TOutput>> {
647
+ const startTime = performance.now();
648
+ const stepResults: WorkflowResult['stepResults'] = [];
649
+ let currentInput: unknown = initialInput;
650
+ let failedSteps = 0;
651
+
652
+ for (const step of steps) {
653
+ const stepStart = performance.now();
654
+ const maxRetries = step.retries || 0;
655
+ let lastError: ExecutionError | undefined;
656
+
657
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
658
+ try {
659
+ let result: unknown;
660
+
661
+ if (step.transform) {
662
+ // Pure transformation step
663
+ result = await step.transform(currentInput as never);
664
+ } else {
665
+ // Service execution step
666
+ let service = step.service;
667
+ if (!service && step.criteria) {
668
+ service = await this.selectService(step.criteria) || undefined;
669
+ }
670
+ if (!service && step.serviceId) {
671
+ service = await this.getServiceById(step.serviceId) || undefined;
672
+ }
673
+
674
+ if (!service) {
675
+ throw this.createError('SERVICE_NOT_FOUND', `No service found for step: ${step.name}`, false);
676
+ }
677
+
678
+ const execResult = await this.execute(service, currentInput, { timeout: step.timeout });
679
+ if (!execResult.success) {
680
+ throw execResult.error;
681
+ }
682
+ result = execResult.data;
683
+ }
684
+
685
+ // Success
686
+ if (step.onSuccess) await step.onSuccess(result as never);
687
+
688
+ stepResults.push({
689
+ stepName: step.name,
690
+ success: true,
691
+ data: result,
692
+ durationMs: Math.round(performance.now() - stepStart),
693
+ });
694
+
695
+ currentInput = result;
696
+ lastError = undefined;
697
+ break;
698
+
699
+ } catch (err) {
700
+ // Check if it's already an ExecutionError
701
+ if (err && typeof err === 'object' && 'code' in err && 'retryable' in err) {
702
+ lastError = err as ExecutionError;
703
+ } else {
704
+ lastError = this.createError('EXECUTION_FAILED', err instanceof Error ? err.message : 'Unknown', false);
705
+ }
706
+
707
+ // Try fallback if available and no retries left
708
+ if (attempt === maxRetries && step.fallback) {
709
+ try {
710
+ const fallbackResult = await this.executeWorkflow([step.fallback], currentInput);
711
+ if (fallbackResult.success) {
712
+ currentInput = fallbackResult.data;
713
+ lastError = undefined;
714
+ stepResults.push({
715
+ stepName: `${step.name} (fallback)`,
716
+ success: true,
717
+ data: fallbackResult.data,
718
+ durationMs: Math.round(performance.now() - stepStart),
719
+ });
720
+ break;
721
+ }
722
+ } catch {
723
+ // Fallback failed, continue with error
724
+ }
725
+ }
726
+ }
727
+ }
728
+
729
+ if (lastError) {
730
+ if (step.onFailure) await step.onFailure(lastError);
731
+
732
+ stepResults.push({
733
+ stepName: step.name,
734
+ success: false,
735
+ error: lastError,
736
+ durationMs: Math.round(performance.now() - stepStart),
737
+ });
738
+
739
+ failedSteps++;
740
+
741
+ // Stop workflow on non-retryable error
742
+ if (!lastError.retryable) break;
743
+ }
744
+ }
745
+
746
+ const completedSteps = stepResults.filter(r => r.success).length;
747
+ const totalMs = Math.round(performance.now() - startTime);
748
+
749
+ return {
750
+ success: failedSteps === 0,
751
+ data: currentInput as TOutput,
752
+ stepResults,
753
+ totalMs,
754
+ completedSteps,
755
+ failedSteps,
756
+ };
757
+ }
758
+
759
+ // ==========================================================================
760
+ // MEMORY - Built-in hooks for agent learning
761
+ // ==========================================================================
762
+
763
+ /**
764
+ * Get agent memory interface
765
+ */
766
+ get memory(): AgentMemory {
767
+ return {
768
+ record: (outcome) => this.recordOutcome(outcome),
769
+ getHistory: (serviceId) => serviceId
770
+ ? this.memoryStore.filter(o => o.serviceId === serviceId)
771
+ : [...this.memoryStore],
772
+ getStats: () => {
773
+ const total = this.memoryStore.length;
774
+ const successful = this.memoryStore.filter(o => o.success).length;
775
+ const avgLatency = total > 0
776
+ ? this.memoryStore.reduce((sum, o) => sum + o.latencyMs, 0) / total
777
+ : 0;
778
+ return {
779
+ totalCalls: total,
780
+ successRate: total > 0 ? successful / total : 0,
781
+ avgLatency: Math.round(avgLatency),
782
+ };
783
+ },
784
+ clear: () => { this.memoryStore = []; },
785
+ };
786
+ }
787
+
788
+ /**
789
+ * Register callback for outcomes (for external memory systems)
790
+ */
791
+ onOutcome(callback: (outcome: OutcomeRecord) => void): () => void {
792
+ this.outcomeCallbacks.push(callback);
793
+ return () => {
794
+ const idx = this.outcomeCallbacks.indexOf(callback);
795
+ if (idx > -1) this.outcomeCallbacks.splice(idx, 1);
796
+ };
797
+ }
798
+
799
+ private outcomeCallbacks: Array<(outcome: OutcomeRecord) => void> = [];
800
+
801
+ private recordOutcome(outcome: OutcomeRecord): void {
802
+ this.memoryStore.push(outcome);
803
+ // Keep last 1000 outcomes
804
+ if (this.memoryStore.length > 1000) {
805
+ this.memoryStore = this.memoryStore.slice(-1000);
806
+ }
807
+ this.outcomeCallbacks.forEach(cb => cb(outcome));
808
+ }
809
+
810
+ // ==========================================================================
811
+ // HELPERS
812
+ // ==========================================================================
813
+
814
+ private async getServiceById(serviceId: string): Promise<SelectedService | null> {
815
+ const response = await fetch(`${this.apiUrl}/api/services/${serviceId}`, {
816
+ headers: this.getHeaders()
817
+ });
818
+ if (!response.ok) return null;
819
+
820
+ const s = await response.json();
821
+ return {
822
+ id: s.id,
823
+ name: s.name,
824
+ endpoint: s.endpointUrl || s.endpoint_url,
825
+ price: s.pricePerCall || '0',
826
+ provider: s.ownerAddress || '',
827
+ reputation: s.reputationScore || 0,
828
+ latency: s.avgLatencyMs || 0,
829
+ selectionReason: 'Direct lookup',
830
+ scoreBreakdown: { reputation: 0, latency: 0, price: 0, total: 0 },
831
+ };
832
+ }
833
+
834
+ private passesConstraints(
835
+ service: { reputation: number; latency: number; price: string; provider: string; verified: boolean },
836
+ constraints: TrustPolicy
837
+ ): boolean {
838
+ if (constraints.minReputation && service.reputation < constraints.minReputation) return false;
839
+ if (constraints.maxLatency && service.latency > constraints.maxLatency) return false;
840
+ if (constraints.maxPrice && parseFloat(service.price) > constraints.maxPrice) return false;
841
+ if (constraints.verifiedOnly && !service.verified) return false;
842
+ if (constraints.blacklistedProviders?.includes(service.provider)) return false;
843
+ return true;
844
+ }
845
+
846
+ private scoreService(
847
+ service: DiscoveredServiceInternal,
848
+ constraints: TrustPolicy
849
+ ): {
850
+ service: DiscoveredServiceInternal;
851
+ score: number;
852
+ repScore: number;
853
+ latScore: number;
854
+ priceScore: number;
855
+ } {
856
+ // Normalize scores to 0-100
857
+ const repScore = service.reputation; // Already 0-100
858
+ const latScore = Math.max(0, 100 - (service.latency / 10)); // Lower is better
859
+ const priceScore = Math.max(0, 100 - (parseFloat(service.price) * 100)); // Lower is better
860
+
861
+ // Boost for preferred providers
862
+ const preferredBoost = constraints.preferredProviders?.includes(service.provider) ? 10 : 0;
863
+
864
+ // Weighted score
865
+ const score = (repScore * 0.5 + latScore * 0.3 + priceScore * 0.2) + preferredBoost;
866
+
867
+ return { service, score, repScore, latScore, priceScore };
868
+ }
869
+
870
+ private explainSelection(scored: { service: { reputation: number; latency: number; price: string }; score: number }): string {
871
+ return `Highest score (${scored.score.toFixed(1)}) based on: reputation=${scored.service.reputation}, ` +
872
+ `latency=${scored.service.latency}ms, price=$${scored.service.price}`;
873
+ }
874
+
875
+ private async makePayment(requirements: {
876
+ payTo: string;
877
+ maxAmountRequired: string;
878
+ asset?: string;
879
+ }): Promise<{ paymentId: string; txHash: string }> {
880
+ if (!this.signer) throw this.createError('UNAUTHORIZED', 'Signer required', false);
881
+
882
+ const signerAddress = await this.signer.getAddress();
883
+ const chainId = NETWORK_CONFIG[this.network].chainId;
884
+
885
+ // EIP-3009 authorization
886
+ const domain = {
887
+ name: 'USD Coin',
888
+ version: '2',
889
+ chainId,
890
+ verifyingContract: requirements.asset || '0xf951eC28187D9E5Ca673Da8FE6757E6f0Be5F77C',
891
+ };
892
+
893
+ const types = {
894
+ TransferWithAuthorization: [
895
+ { name: 'from', type: 'address' },
896
+ { name: 'to', type: 'address' },
897
+ { name: 'value', type: 'uint256' },
898
+ { name: 'validAfter', type: 'uint256' },
899
+ { name: 'validBefore', type: 'uint256' },
900
+ { name: 'nonce', type: 'bytes32' },
901
+ ],
902
+ };
903
+
904
+ const nonce = ethers.hexlify(ethers.randomBytes(32));
905
+ const validAfter = 0;
906
+ const validBefore = Math.floor(Date.now() / 1000) + 3600;
907
+
908
+ const value = {
909
+ from: signerAddress,
910
+ to: requirements.payTo,
911
+ value: ethers.parseUnits(requirements.maxAmountRequired, 6),
912
+ validAfter,
913
+ validBefore,
914
+ nonce,
915
+ };
916
+
917
+ const signature = await (this.signer as ethers.Signer & {
918
+ signTypedData: (d: typeof domain, t: typeof types, v: typeof value) => Promise<string>;
919
+ }).signTypedData(domain, types, value);
920
+
921
+ const response = await fetch(`${FACILITATOR_URL}/settle`, {
922
+ method: 'POST',
923
+ headers: { 'Content-Type': 'application/json' },
924
+ body: JSON.stringify({
925
+ from: signerAddress,
926
+ to: requirements.payTo,
927
+ value: requirements.maxAmountRequired,
928
+ validAfter,
929
+ validBefore,
930
+ nonce,
931
+ signature,
932
+ network: this.network === 'cronos-mainnet' ? 'cronos-mainnet' : 'cronos-testnet',
933
+ }),
934
+ });
935
+
936
+ if (!response.ok) {
937
+ throw this.createError('PAYMENT_FAILED', 'Payment settlement failed', true, 5000);
938
+ }
939
+
940
+ const result = await response.json();
941
+ return {
942
+ paymentId: `pay_${result.txHash?.slice(2, 18) || Date.now()}`,
943
+ txHash: result.txHash || '',
944
+ };
945
+ }
946
+
947
+ // ==========================================================================
948
+ // A2A DISCOVERY - Agent-to-Agent Protocol Support
949
+ // ==========================================================================
950
+
951
+ /**
952
+ * Discover an agent's capabilities via their agent card
953
+ *
954
+ * @example
955
+ * const card = await agent.discoverAgentCard("https://api.service.com");
956
+ * console.log(card.resources); // Available x402-gated resources
957
+ */
958
+ async discoverAgentCard(baseUrl: string): Promise<AgentCard | null> {
959
+ const paths = ['/.well-known/agent-card.json', '/.well-known/agent.json'];
960
+
961
+ for (const path of paths) {
962
+ try {
963
+ const response = await fetch(`${baseUrl.replace(/\/$/, '')}${path}`, {
964
+ headers: { 'Accept': 'application/json' },
965
+ });
966
+
967
+ if (response.ok) {
968
+ return await response.json();
969
+ }
970
+ } catch {
971
+ continue;
972
+ }
973
+ }
974
+
975
+ return null;
976
+ }
977
+
978
+ /**
979
+ * Discover multiple remote agents in parallel
980
+ *
981
+ * @example
982
+ * const agents = await agent.discoverRemoteAgents([
983
+ * "https://perpai.relaycore.xyz",
984
+ * "https://rwa.relaycore.xyz"
985
+ * ]);
986
+ */
987
+ async discoverRemoteAgents(urls: string[]): Promise<Array<{
988
+ url: string;
989
+ card: AgentCard | null;
990
+ online: boolean;
991
+ }>> {
992
+ return Promise.all(
993
+ urls.map(async (url) => {
994
+ const card = await this.discoverAgentCard(url);
995
+ return {
996
+ url,
997
+ card,
998
+ online: card !== null,
999
+ };
1000
+ })
1001
+ );
1002
+ }
1003
+
1004
+ /**
1005
+ * Get local Relay Core agent card
1006
+ */
1007
+ async getLocalAgentCard(): Promise<AgentCard> {
1008
+ const response = await fetch(`${this.apiUrl}/.well-known/agent-card.json`);
1009
+ if (!response.ok) {
1010
+ throw this.createError('SERVICE_UNAVAILABLE', 'Failed to fetch agent card', true);
1011
+ }
1012
+ return response.json();
1013
+ }
1014
+
1015
+ // ==========================================================================
1016
+ // TASK ARTIFACTS - Track and audit all agent actions
1017
+ // ==========================================================================
1018
+
1019
+ /**
1020
+ * Create a new task artifact
1021
+ *
1022
+ * @example
1023
+ * const task = await agent.createTask({
1024
+ * service_id: "perpai-quote",
1025
+ * inputs: { pair: "BTC/USD" }
1026
+ * });
1027
+ */
1028
+ async createTask(params: {
1029
+ service_id?: string;
1030
+ session_id?: string;
1031
+ inputs: Record<string, unknown>;
1032
+ }): Promise<TaskArtifact> {
1033
+ const agentId = await this.getAddress();
1034
+
1035
+ const response = await fetch(`${this.apiUrl}/api/tasks`, {
1036
+ method: 'POST',
1037
+ headers: this.getHeaders(),
1038
+ body: JSON.stringify({
1039
+ agent_id: agentId,
1040
+ service_id: params.service_id,
1041
+ session_id: params.session_id,
1042
+ inputs: params.inputs,
1043
+ }),
1044
+ });
1045
+
1046
+ if (!response.ok) {
1047
+ throw this.createError('EXECUTION_FAILED', 'Failed to create task', true);
1048
+ }
1049
+
1050
+ return response.json();
1051
+ }
1052
+
1053
+ /**
1054
+ * Get a task artifact by ID
1055
+ */
1056
+ async getTask(taskId: string): Promise<TaskArtifact | null> {
1057
+ const response = await fetch(`${this.apiUrl}/api/tasks/${taskId}`, {
1058
+ headers: this.getHeaders()
1059
+ });
1060
+ if (response.status === 404) return null;
1061
+ if (!response.ok) {
1062
+ throw this.createError('EXECUTION_FAILED', 'Failed to get task', true);
1063
+ }
1064
+ return response.json();
1065
+ }
1066
+
1067
+ /**
1068
+ * Query task artifacts
1069
+ *
1070
+ * @example
1071
+ * const tasks = await agent.getTasks({ state: 'settled', limit: 10 });
1072
+ */
1073
+ async getTasks(params?: {
1074
+ service_id?: string;
1075
+ session_id?: string;
1076
+ state?: TaskState;
1077
+ from?: Date;
1078
+ to?: Date;
1079
+ limit?: number;
1080
+ }): Promise<TaskArtifact[]> {
1081
+ const agentId = await this.getAddress();
1082
+ const queryParams = new URLSearchParams();
1083
+ queryParams.set('agent_id', agentId);
1084
+
1085
+ if (params?.service_id) queryParams.set('service_id', params.service_id);
1086
+ if (params?.session_id) queryParams.set('session_id', params.session_id.toString());
1087
+ if (params?.state) queryParams.set('state', params.state);
1088
+ if (params?.from) queryParams.set('from', params.from.toISOString());
1089
+ if (params?.to) queryParams.set('to', params.to.toISOString());
1090
+ if (params?.limit) queryParams.set('limit', params.limit.toString());
1091
+
1092
+ const response = await fetch(`${this.apiUrl}/api/tasks?${queryParams}`, {
1093
+ headers: this.getHeaders()
1094
+ });
1095
+ if (!response.ok) {
1096
+ throw this.createError('EXECUTION_FAILED', 'Failed to query tasks', true);
1097
+ }
1098
+
1099
+ const data = await response.json();
1100
+ return data.tasks || [];
1101
+ }
1102
+
1103
+ /**
1104
+ * Get task statistics
1105
+ */
1106
+ async getTaskStats(): Promise<TaskStats> {
1107
+ const agentId = await this.getAddress();
1108
+ const response = await fetch(`${this.apiUrl}/api/tasks/stats?agent_id=${agentId}`, {
1109
+ headers: this.getHeaders()
1110
+ });
1111
+ if (!response.ok) {
1112
+ throw this.createError('EXECUTION_FAILED', 'Failed to get task stats', true);
1113
+ }
1114
+ return response.json();
1115
+ }
1116
+
1117
+ /**
1118
+ * Mark a task as settled (success)
1119
+ */
1120
+ async settleTask(taskId: string, outputs: Record<string, unknown>, metrics?: TaskArtifact['metrics']): Promise<TaskArtifact> {
1121
+ const response = await fetch(`${this.apiUrl}/api/tasks/${taskId}/settle`, {
1122
+ method: 'POST',
1123
+ headers: this.getHeaders(),
1124
+ body: JSON.stringify({ outputs, metrics }),
1125
+ });
1126
+
1127
+ if (!response.ok) {
1128
+ throw this.createError('EXECUTION_FAILED', 'Failed to settle task', true);
1129
+ }
1130
+
1131
+ return response.json();
1132
+ }
1133
+
1134
+ /**
1135
+ * Mark a task as failed
1136
+ */
1137
+ async failTask(taskId: string, error: { code: string; message: string; retryable: boolean }, metrics?: TaskArtifact['metrics']): Promise<TaskArtifact> {
1138
+ const response = await fetch(`${this.apiUrl}/api/tasks/${taskId}/fail`, {
1139
+ method: 'POST',
1140
+ headers: this.getHeaders(),
1141
+ body: JSON.stringify({ error, metrics }),
1142
+ });
1143
+
1144
+ if (!response.ok) {
1145
+ throw this.createError('EXECUTION_FAILED', 'Failed to fail task', true);
1146
+ }
1147
+
1148
+ return response.json();
1149
+ }
1150
+
1151
+ // ========================================================================
1152
+ // META-AGENT METHODS (Agent Discovery & Hiring)
1153
+ // ========================================================================
1154
+
1155
+ /**
1156
+ * Discover agents based on criteria
1157
+ */
1158
+ async discoverAgents(query: {
1159
+ capability?: string;
1160
+ category?: string;
1161
+ minReputation?: number;
1162
+ maxPricePerCall?: string;
1163
+ limit?: number;
1164
+ }): Promise<Array<{
1165
+ agentId: string;
1166
+ agentName: string;
1167
+ agentUrl: string;
1168
+ reputationScore: number;
1169
+ pricePerCall: string;
1170
+ successRate: number;
1171
+ compositeScore: number;
1172
+ }>> {
1173
+ const response = await fetch(`${this.apiUrl}/api/meta-agent/discover`, {
1174
+ method: 'POST',
1175
+ headers: this.getHeaders(),
1176
+ body: JSON.stringify(query)
1177
+ });
1178
+
1179
+ if (!response.ok) {
1180
+ throw this.createError('EXECUTION_FAILED', 'Agent discovery failed', true);
1181
+ }
1182
+
1183
+ const data = await response.json();
1184
+ return data.agents || [];
1185
+ }
1186
+
1187
+ /**
1188
+ * Hire an agent to perform a task
1189
+ */
1190
+ async hireAgent(request: {
1191
+ agentId: string;
1192
+ resourceId: string;
1193
+ budget: string;
1194
+ task: Record<string, unknown>;
1195
+ }): Promise<{
1196
+ success: boolean;
1197
+ taskId: string;
1198
+ agentId: string;
1199
+ cost: string;
1200
+ }> {
1201
+ const agentId = await this.getAddress();
1202
+
1203
+ const response = await fetch(`${this.apiUrl}/api/meta-agent/hire`, {
1204
+ method: 'POST',
1205
+ headers: {
1206
+ ...this.getHeaders(),
1207
+ 'X-Agent-Id': agentId
1208
+ },
1209
+ body: JSON.stringify(request)
1210
+ });
1211
+
1212
+ if (!response.ok) {
1213
+ const error = await response.json();
1214
+ throw this.createError('EXECUTION_FAILED', error.error || 'Agent hiring failed', true);
1215
+ }
1216
+
1217
+ return response.json();
1218
+ }
1219
+
1220
+ /**
1221
+ * Execute a delegated task
1222
+ */
1223
+ async executeDelegation(taskId: string): Promise<{
1224
+ success: boolean;
1225
+ outcome: {
1226
+ taskId: string;
1227
+ agentId: string;
1228
+ state: string;
1229
+ outputs?: Record<string, unknown>;
1230
+ error?: { code: string; message: string };
1231
+ };
1232
+ }> {
1233
+ const response = await fetch(`${this.apiUrl}/api/meta-agent/execute/${taskId}`, {
1234
+ method: 'POST',
1235
+ headers: this.getHeaders()
1236
+ });
1237
+
1238
+ if (!response.ok) {
1239
+ throw this.createError('EXECUTION_FAILED', 'Delegation execution failed', true);
1240
+ }
1241
+
1242
+ return response.json();
1243
+ }
1244
+
1245
+ /**
1246
+ * Get delegation status
1247
+ */
1248
+ async getDelegationStatus(taskId: string): Promise<{
1249
+ taskId: string;
1250
+ agentId: string;
1251
+ state: string;
1252
+ cost: string;
1253
+ outputs?: Record<string, unknown>;
1254
+ }> {
1255
+ const response = await fetch(`${this.apiUrl}/api/meta-agent/status/${taskId}`, {
1256
+ headers: this.getHeaders()
1257
+ });
1258
+
1259
+ if (!response.ok) {
1260
+ throw this.createError('EXECUTION_FAILED', 'Failed to get delegation status', true);
1261
+ }
1262
+
1263
+ const data = await response.json();
1264
+ return data.outcome;
1265
+ }
1266
+
1267
+ /**
1268
+ * Fetch agent card for a specific agent
1269
+ */
1270
+ async getAgentCard(agentId: string): Promise<{
1271
+ name: string;
1272
+ description: string;
1273
+ url: string;
1274
+ network: string;
1275
+ resources: Array<{
1276
+ id: string;
1277
+ title: string;
1278
+ url: string;
1279
+ }>;
1280
+ } | null> {
1281
+ const response = await fetch(`${this.apiUrl}/api/meta-agent/agent-card/${agentId}`, {
1282
+ headers: this.getHeaders()
1283
+ });
1284
+
1285
+ if (!response.ok) {
1286
+ return null;
1287
+ }
1288
+
1289
+ const data = await response.json();
1290
+ return data.card || null;
1291
+ }
1292
+
1293
+ // ========================================================================
1294
+ // SESSION MANAGEMENT (x402)
1295
+ // ========================================================================
1296
+
1297
+ /**
1298
+ * Create a new x402 session
1299
+ *
1300
+ * @returns Session ID and payment request to activate it
1301
+ */
1302
+ async createSession(params: {
1303
+ maxSpend: number;
1304
+ durationHours: number;
1305
+ authorizedAgents?: string[];
1306
+ }): Promise<{
1307
+ sessionId: string;
1308
+ paymentRequest: {
1309
+ amount: string;
1310
+ payTo: string;
1311
+ asset: string;
1312
+ };
1313
+ }> {
1314
+ const ownerAddress = await this.getAddress();
1315
+
1316
+ const response = await fetch(`${this.apiUrl}/api/sessions/create`, {
1317
+ method: 'POST',
1318
+ headers: this.getHeaders(),
1319
+ body: JSON.stringify({
1320
+ ownerAddress,
1321
+ maxSpend: params.maxSpend,
1322
+ durationHours: params.durationHours,
1323
+ authorizedAgents: params.authorizedAgents || []
1324
+ })
1325
+ });
1326
+
1327
+ if (!response.ok) {
1328
+ const error = await response.json();
1329
+ throw this.createError('EXECUTION_FAILED', error.message || 'Failed to create session', false);
1330
+ }
1331
+
1332
+ const result = await response.json();
1333
+ return {
1334
+ sessionId: result.session.session_id,
1335
+ paymentRequest: result.paymentRequest
1336
+ };
1337
+ }
1338
+
1339
+ /**
1340
+ * Activate a session after paying USDC
1341
+ */
1342
+ async activateSession(sessionId: string, txHash: string, amount: string): Promise<{ success: boolean }> {
1343
+ const response = await fetch(`${this.apiUrl}/api/sessions/${sessionId}/activate`, {
1344
+ method: 'POST',
1345
+ headers: this.getHeaders(),
1346
+ body: JSON.stringify({ txHash, amount })
1347
+ });
1348
+
1349
+ if (!response.ok) {
1350
+ const error = await response.json();
1351
+ throw this.createError('EXECUTION_FAILED', error.message || 'Failed to activate session', true);
1352
+ }
1353
+
1354
+ return { success: true };
1355
+ }
1356
+
1357
+ /**
1358
+ * Get session details
1359
+ */
1360
+ async getSession(sessionId: string): Promise<{
1361
+ id: string;
1362
+ owner: string;
1363
+ maxSpend: string;
1364
+ spent: string;
1365
+ isActive: boolean;
1366
+ expiresAt: string;
1367
+ } | null> {
1368
+ const response = await fetch(`${this.apiUrl}/api/sessions/${sessionId}`, {
1369
+ headers: this.getHeaders()
1370
+ });
1371
+
1372
+ if (response.status === 404) return null;
1373
+ if (!response.ok) {
1374
+ throw this.createError('EXECUTION_FAILED', 'Failed to get session', true);
1375
+ }
1376
+
1377
+ const data = await response.json();
1378
+ const session = data.session;
1379
+ return {
1380
+ id: session.session_id,
1381
+ owner: session.owner_address,
1382
+ maxSpend: session.max_spend,
1383
+ spent: session.spent,
1384
+ isActive: session.is_active,
1385
+ expiresAt: session.expires_at
1386
+ };
1387
+ }
1388
+
1389
+
1390
+ private createError(
1391
+ code: ErrorCode,
1392
+ message: string,
1393
+ retryable: boolean,
1394
+ retryAfterMs?: number
1395
+ ): ExecutionError {
1396
+ return { code, message, retryable, retryAfterMs };
1397
+ }
1398
+ }
1399
+
1400
+ // ============================================================================
1401
+ // FACTORY & EXPORTS
1402
+ // ============================================================================
1403
+
1404
+ /**
1405
+ * Create a Relay Agent instance
1406
+ *
1407
+ * @example
1408
+ * const agent = createAgent({ wallet, network: "cronos-testnet" });
1409
+ */
1410
+ export function createAgent(config: AgentConfig): RelayAgent {
1411
+ return new RelayAgent(config);
1412
+ }
1413
+
1414
+ export default RelayAgent;