@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/lib/x402.ts ADDED
@@ -0,0 +1,220 @@
1
+ /**
2
+ * X402 Payment Middleware - Cronos Standard Implementation
3
+ *
4
+ * Based on x402-examples reference from Cronos Labs.
5
+ * Provides middleware for protecting routes with x402 payment requirements.
6
+ */
7
+
8
+ import crypto from 'crypto';
9
+ import type { Request, Response, NextFunction } from 'express';
10
+ import type { Facilitator, VerifyRequest } from '@crypto.com/facilitator-client';
11
+ import type {
12
+ X402Accepts,
13
+ X402Response,
14
+ X402PaidRecord,
15
+ X402PayResult,
16
+ X402ProtectionOptions,
17
+ } from '../types/x402.types';
18
+
19
+ /**
20
+ * In-memory entitlement store keyed by payment ID.
21
+ *
22
+ * @remarks
23
+ * For production, replace with persistent storage (Redis, Supabase, etc.)
24
+ */
25
+ const paidEntitlements = new Map<string, X402PaidRecord>();
26
+
27
+ /**
28
+ * Generates a unique payment identifier per Cronos standard.
29
+ * @returns Payment ID in format `pay_<uuid>`
30
+ */
31
+ export const generatePaymentId = (): string => `pay_${crypto.randomUUID()}`;
32
+
33
+ /**
34
+ * Check if a payment ID has been settled.
35
+ */
36
+ export function isEntitled(paymentId: string): boolean {
37
+ return paidEntitlements.get(paymentId)?.settled === true;
38
+ }
39
+
40
+ /**
41
+ * Get entitlement record for a payment ID.
42
+ */
43
+ export function getEntitlement(paymentId: string): X402PaidRecord | undefined {
44
+ return paidEntitlements.get(paymentId);
45
+ }
46
+
47
+ /**
48
+ * Store an entitlement record after successful payment.
49
+ */
50
+ export function recordEntitlement(paymentId: string, record: X402PaidRecord): void {
51
+ paidEntitlements.set(paymentId, record);
52
+ }
53
+
54
+ /**
55
+ * Creates Express middleware that enforces x402 payment.
56
+ *
57
+ * If the request is already entitled (has valid x-payment-id), calls next().
58
+ * Otherwise, responds with HTTP 402 and x402 challenge.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * router.get('/protected', requireX402({
63
+ * network: 'cronos-testnet',
64
+ * payTo: '0x...',
65
+ * asset: '0x...',
66
+ * maxAmountRequired: '1000000',
67
+ * description: 'Access to premium data',
68
+ * resource: '/api/protected',
69
+ * }), handler);
70
+ * ```
71
+ */
72
+ export function requireX402(options: X402ProtectionOptions) {
73
+ const {
74
+ network,
75
+ payTo,
76
+ asset,
77
+ maxAmountRequired,
78
+ maxTimeoutSeconds = 300,
79
+ description,
80
+ mimeType = 'application/json',
81
+ resource,
82
+ outputSchema,
83
+ getEntitlementKey,
84
+ } = options;
85
+
86
+ return (req: Request, res: Response, next: NextFunction): void => {
87
+ // Check for existing entitlement
88
+ const entitlementKey = (
89
+ getEntitlementKey?.(req) ??
90
+ req.header('x-payment-id') ??
91
+ ''
92
+ ).trim();
93
+
94
+ if (entitlementKey && isEntitled(entitlementKey)) {
95
+ next();
96
+ return;
97
+ }
98
+
99
+ // Generate new payment ID for challenge
100
+ const paymentId = generatePaymentId();
101
+
102
+ const accepts: X402Accepts = {
103
+ scheme: 'exact',
104
+ network,
105
+ asset,
106
+ payTo,
107
+ maxAmountRequired,
108
+ maxTimeoutSeconds,
109
+ description,
110
+ mimeType,
111
+ resource,
112
+ outputSchema,
113
+ extra: { paymentId },
114
+ };
115
+
116
+ const response: X402Response = {
117
+ x402Version: 1,
118
+ error: 'payment_required',
119
+ accepts: [accepts],
120
+ };
121
+
122
+ res.status(402).json(response);
123
+ };
124
+ }
125
+
126
+ /**
127
+ * Verifies and settles an x402 payment using the Facilitator SDK.
128
+ * Records entitlement on success.
129
+ *
130
+ * @example
131
+ * ```ts
132
+ * const result = await handleX402Settlement({
133
+ * facilitator,
134
+ * paymentId: 'pay_...',
135
+ * paymentHeader: '...',
136
+ * paymentRequirements: {...},
137
+ * });
138
+ * ```
139
+ */
140
+ export async function handleX402Settlement(params: {
141
+ facilitator: Facilitator;
142
+ paymentId: string;
143
+ paymentHeader: string;
144
+ paymentRequirements: VerifyRequest['paymentRequirements'];
145
+ }): Promise<X402PayResult> {
146
+ const { facilitator, paymentId, paymentHeader, paymentRequirements } = params;
147
+
148
+ const body: VerifyRequest = {
149
+ x402Version: 1,
150
+ paymentHeader,
151
+ paymentRequirements,
152
+ };
153
+
154
+ // Step 1: Verify
155
+ const verify = await facilitator.verifyPayment(body);
156
+ if (!verify.isValid) {
157
+ return {
158
+ ok: false,
159
+ error: 'verify_failed',
160
+ details: verify,
161
+ };
162
+ }
163
+
164
+ // Step 2: Settle
165
+ const settle = await facilitator.settlePayment(body);
166
+ if (settle.event !== 'payment.settled') {
167
+ return {
168
+ ok: false,
169
+ error: 'settle_failed',
170
+ details: settle,
171
+ };
172
+ }
173
+
174
+ // Step 3: Record entitlement
175
+ recordEntitlement(paymentId, {
176
+ settled: true,
177
+ txHash: settle.txHash,
178
+ at: Date.now(),
179
+ });
180
+
181
+ return {
182
+ ok: true,
183
+ txHash: settle.txHash,
184
+ };
185
+ }
186
+
187
+ /**
188
+ * Helper to create payment requirements for a resource.
189
+ */
190
+ export function createPaymentRequirements(options: {
191
+ network: string;
192
+ payTo: string;
193
+ asset: string;
194
+ maxAmountRequired: string;
195
+ resource: string;
196
+ description: string;
197
+ }): X402Accepts {
198
+ return {
199
+ scheme: 'exact',
200
+ network: options.network as X402Accepts['network'],
201
+ payTo: options.payTo,
202
+ asset: options.asset,
203
+ maxAmountRequired: options.maxAmountRequired,
204
+ maxTimeoutSeconds: 300,
205
+ description: options.description,
206
+ mimeType: 'application/json',
207
+ resource: options.resource,
208
+ extra: { paymentId: generatePaymentId() },
209
+ };
210
+ }
211
+
212
+ export default {
213
+ requireX402,
214
+ handleX402Settlement,
215
+ generatePaymentId,
216
+ isEntitled,
217
+ getEntitlement,
218
+ recordEntitlement,
219
+ createPaymentRequirements,
220
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@relaycore/sdk",
3
+ "version": "1.0.0",
4
+ "description": "The official SDK for Relay Core - Build AI Agents and Services on Cronos",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsup",
10
+ "lint": "eslint .",
11
+ "test": "vitest"
12
+ },
13
+ "keywords": [
14
+ "relaycore",
15
+ "cronos",
16
+ "ai",
17
+ "agents",
18
+ "sdk",
19
+ "x402"
20
+ ],
21
+ "author": "Relay Core Team",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "@crypto.com/facilitator-client": "^1.0.2",
25
+ "@reown/appkit": "^1.8.16",
26
+ "ethers": "^6.13.4",
27
+ "siwe": "^3.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/express": "^5.0.6",
31
+ "tsup": "^8.3.5",
32
+ "typescript": "^5.6.3",
33
+ "vitest": "^2.1.8"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ }
38
+ }
@@ -0,0 +1,311 @@
1
+
2
+ /**
3
+ * Service Provider SDK
4
+ *
5
+ * For service providers to register services, handle payments,
6
+ * and track their reputation on the Relay Core platform.
7
+ */
8
+
9
+ // Configuration for the SDK
10
+ export interface ProviderSDKConfig {
11
+ apiUrl?: string;
12
+ supabaseUrl?: string;
13
+ supabaseKey?: string;
14
+ }
15
+
16
+ // Service registration parameters
17
+ export interface ServiceRegistration {
18
+ name: string;
19
+ description: string;
20
+ category: string;
21
+ endpointUrl: string;
22
+ pricePerCall: string;
23
+ inputSchema?: Record<string, unknown>;
24
+ outputSchema?: Record<string, unknown>;
25
+ inputType?: string;
26
+ outputType?: string;
27
+ tags?: string[];
28
+ capabilities?: string[];
29
+ }
30
+
31
+ // Registered service response
32
+ export interface RegisteredService {
33
+ id: string;
34
+ name: string;
35
+ endpointUrl: string;
36
+ ownerAddress: string;
37
+ createdAt: string;
38
+ }
39
+
40
+ // Reputation data
41
+ export interface ProviderReputation {
42
+ reputationScore: number;
43
+ successRate: number;
44
+ totalPayments: number;
45
+ avgLatencyMs: number;
46
+ trend: 'improving' | 'stable' | 'declining';
47
+ }
48
+
49
+ // Payment received event
50
+ export interface PaymentReceived {
51
+ paymentId: string;
52
+ txHash: string;
53
+ amount: string;
54
+ payerAddress: string;
55
+ timestamp: string;
56
+ }
57
+
58
+ // Metrics snapshot
59
+ export interface ServiceMetrics {
60
+ timestamp: string;
61
+ successRate: number;
62
+ avgLatencyMs: number;
63
+ callVolume: number;
64
+ reputationScore: number;
65
+ }
66
+
67
+ const DEFAULT_API_URL = 'https://api.relaycore.xyz';
68
+
69
+ export class ServiceProviderSDK {
70
+ private apiUrl: string;
71
+ private walletAddress: string;
72
+
73
+ constructor(walletAddress: string, config: ProviderSDKConfig = {}) {
74
+ this.walletAddress = walletAddress.toLowerCase();
75
+ this.apiUrl = config.apiUrl || DEFAULT_API_URL;
76
+ }
77
+
78
+ /**
79
+ * Register a new service
80
+ */
81
+ async registerService(service: ServiceRegistration): Promise<RegisteredService> {
82
+ const response = await fetch(`${this.apiUrl}/api/services`, {
83
+ method: 'POST',
84
+ headers: {
85
+ 'Content-Type': 'application/json',
86
+ },
87
+ body: JSON.stringify({
88
+ ...service,
89
+ ownerAddress: this.walletAddress,
90
+ }),
91
+ });
92
+
93
+ if (!response.ok) {
94
+ const error = await response.json();
95
+ throw new Error(`Failed to register service: ${error.error || response.statusText}`);
96
+ }
97
+
98
+ const data = await response.json();
99
+ return {
100
+ id: data.id,
101
+ name: data.name,
102
+ endpointUrl: data.endpointUrl,
103
+ ownerAddress: this.walletAddress,
104
+ createdAt: new Date().toISOString(),
105
+ };
106
+ }
107
+
108
+ /**
109
+ * Update an existing service
110
+ */
111
+ async updateService(
112
+ serviceId: string,
113
+ updates: Partial<ServiceRegistration>
114
+ ): Promise<void> {
115
+ const response = await fetch(`${this.apiUrl}/api/services/${serviceId}`, {
116
+ method: 'PUT',
117
+ headers: {
118
+ 'Content-Type': 'application/json',
119
+ },
120
+ body: JSON.stringify(updates),
121
+ });
122
+
123
+ if (!response.ok) {
124
+ const error = await response.json();
125
+ throw new Error(`Failed to update service: ${error.error || response.statusText}`);
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Deactivate a service
131
+ */
132
+ async deactivateService(serviceId: string): Promise<void> {
133
+ await this.updateService(serviceId, { isActive: false } as any);
134
+ }
135
+
136
+ /**
137
+ * Get current reputation
138
+ */
139
+ async getReputation(): Promise<ProviderReputation> {
140
+ const response = await fetch(
141
+ `${this.apiUrl}/api/services?ownerAddress=${this.walletAddress}&limit=1`
142
+ );
143
+
144
+ if (!response.ok) {
145
+ throw new Error('Failed to fetch reputation');
146
+ }
147
+
148
+ const data = await response.json();
149
+ const service = data.services?.[0];
150
+
151
+ if (!service) {
152
+ return {
153
+ reputationScore: 0,
154
+ successRate: 0,
155
+ totalPayments: 0,
156
+ avgLatencyMs: 0,
157
+ trend: 'stable',
158
+ };
159
+ }
160
+
161
+ return {
162
+ reputationScore: service.reputationScore || 0,
163
+ successRate: service.successRate || 0,
164
+ totalPayments: service.totalPayments || 0,
165
+ avgLatencyMs: service.avgLatencyMs || 0,
166
+ trend: service.trend || 'stable',
167
+ };
168
+ }
169
+
170
+ /**
171
+ * Get service metrics history
172
+ */
173
+ async getMetricsHistory(
174
+ serviceId: string,
175
+ options: { from?: Date; to?: Date } = {}
176
+ ): Promise<ServiceMetrics[]> {
177
+ const params = new URLSearchParams();
178
+ if (options.from) params.set('from', options.from.toISOString());
179
+ if (options.to) params.set('to', options.to.toISOString());
180
+
181
+ const response = await fetch(
182
+ `${this.apiUrl}/api/services/${serviceId}/metrics?${params}`
183
+ );
184
+
185
+ if (!response.ok) {
186
+ throw new Error('Failed to fetch metrics');
187
+ }
188
+
189
+ const data = await response.json();
190
+ return data.data || [];
191
+ }
192
+
193
+ /**
194
+ * Get all registered services for this provider
195
+ */
196
+ async getMyServices(): Promise<RegisteredService[]> {
197
+ const response = await fetch(
198
+ `${this.apiUrl}/api/services?ownerAddress=${this.walletAddress}`
199
+ );
200
+
201
+ if (!response.ok) {
202
+ throw new Error('Failed to fetch services');
203
+ }
204
+
205
+ const data = await response.json();
206
+ return (data.services || []).map((s: Record<string, unknown>) => ({
207
+ id: s.id,
208
+ name: s.name,
209
+ endpointUrl: s.endpointUrl,
210
+ ownerAddress: s.ownerAddress,
211
+ createdAt: s.createdAt,
212
+ }));
213
+ }
214
+
215
+ /**
216
+ * Generate x402 payment requirements for an endpoint
217
+ * Use this when building your service's 402 response
218
+ */
219
+ generatePaymentRequirements(params: {
220
+ amount: string;
221
+ resourceUrl: string;
222
+ description?: string;
223
+ timeoutSeconds?: number;
224
+ }): {
225
+ x402Version: number;
226
+ paymentRequirements: {
227
+ scheme: 'exact';
228
+ network: string;
229
+ payTo: string;
230
+ asset: string;
231
+ maxAmountRequired: string;
232
+ maxTimeoutSeconds: number;
233
+ description?: string;
234
+ };
235
+ } {
236
+ return {
237
+ x402Version: 1,
238
+ paymentRequirements: {
239
+ scheme: 'exact',
240
+ network: 'cronos-mainnet',
241
+ payTo: this.walletAddress,
242
+ asset: '0xf951eC28187D9E5Ca673Da8FE6757E6f0Be5F77C', // USDC.e on Cronos
243
+ maxAmountRequired: params.amount,
244
+ maxTimeoutSeconds: params.timeoutSeconds || 60,
245
+ description: params.description,
246
+ },
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Verify a payment was made (for use in your service)
252
+ */
253
+ async verifyPayment(paymentId: string): Promise<{
254
+ verified: boolean;
255
+ amount?: string;
256
+ payerAddress?: string;
257
+ }> {
258
+ const response = await fetch(`${this.apiUrl}/api/payments/${paymentId}`);
259
+
260
+ if (!response.ok) {
261
+ return { verified: false };
262
+ }
263
+
264
+ const data = await response.json();
265
+ return {
266
+ verified: data.payment?.status === 'settled',
267
+ amount: data.payment?.amount,
268
+ payerAddress: data.payment?.payerAddress,
269
+ };
270
+ }
271
+
272
+ /**
273
+ * Record a service outcome (success or failure)
274
+ * Call after each service invocation
275
+ */
276
+ async recordOutcome(params: {
277
+ paymentId: string;
278
+ outcomeType: 'delivered' | 'failed' | 'partial';
279
+ latencyMs: number;
280
+ evidence?: Record<string, unknown>;
281
+ }): Promise<void> {
282
+ const response = await fetch(`${this.apiUrl}/api/outcomes`, {
283
+ method: 'POST',
284
+ headers: {
285
+ 'Content-Type': 'application/json',
286
+ },
287
+ body: JSON.stringify({
288
+ paymentId: params.paymentId,
289
+ outcomeType: params.outcomeType,
290
+ latencyMs: params.latencyMs,
291
+ evidence: params.evidence,
292
+ }),
293
+ });
294
+
295
+ if (!response.ok) {
296
+ console.error('Failed to record outcome');
297
+ }
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Create a Service Provider SDK instance
303
+ */
304
+ export function createProviderSDK(
305
+ walletAddress: string,
306
+ config?: ProviderSDKConfig
307
+ ): ServiceProviderSDK {
308
+ return new ServiceProviderSDK(walletAddress, config);
309
+ }
310
+
311
+ export default ServiceProviderSDK;