@oobe-protocol-labs/synapse-sap-sdk 0.10.1 → 0.11.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,1278 @@
1
+ /**
2
+ * @module registries/fairscale
3
+ * @description FairScale reputation aggregation registry.
4
+ *
5
+ * Wraps both FairScale REST APIs:
6
+ * - **Agent & Credit API** (`agent-api.fairscale.xyz`) — agent trust score,
7
+ * trust gate, batch scoring, composable score, agent profile, score
8
+ * history, directory, leaderboard, credit assessment.
9
+ * - **Human Score API** (`api.fairscale.xyz`) — human wallet fingerprint,
10
+ * FairScore, on-chain features, badges.
11
+ *
12
+ * The killer feature is {@link FairScaleRegistry.aggregate}: it merges
13
+ * SAP's **on-chain** reputation (`AgentAccount.reputationScore` +
14
+ * feedback count + activity signals) with FairScale's **off-chain**
15
+ * trust score into a single normalised, weight-tunable signal. Apps
16
+ * therefore get a multi-source reputation rather than relying on either
17
+ * registry alone.
18
+ *
19
+ * Zero runtime dependencies — uses native `fetch` (Node ≥18, browsers,
20
+ * Edge runtimes). Auth via `fairkey` (or `X-Api-Key` for credit).
21
+ *
22
+ * @category Registries
23
+ * @since v0.11.0
24
+ *
25
+ * @example Standalone usage (just the FairScale wrapper)
26
+ * ```ts
27
+ * const fs = client.fairscale;
28
+ * const score = await fs.score(agentWallet);
29
+ * const allowed = await fs.trustGate(agentWallet, { minScore: 60 });
30
+ * const profile = await fs.agentProfile(agentWallet);
31
+ * const human = await fs.human.score(userWallet);
32
+ * ```
33
+ *
34
+ * @example Aggregated reputation (SAP + FairScale)
35
+ * ```ts
36
+ * const merged = await client.fairscale.aggregate(agentWallet, {
37
+ * weights: { sap: 0.4, fairscale: 0.6 },
38
+ * require: { sapMinFeedbacks: 1 },
39
+ * });
40
+ * console.log(merged.combined.score, merged.combined.tier);
41
+ * ```
42
+ */
43
+
44
+ import type { PublicKey } from "@solana/web3.js";
45
+ import { SapError } from "../errors";
46
+ import type { SapProgram } from "../modules/base";
47
+ import { DiscoveryRegistry, type AgentProfile } from "./discovery";
48
+
49
+ // ═══════════════════════════════════════════════════════════════════
50
+ // Errors
51
+ // ═══════════════════════════════════════════════════════════════════
52
+
53
+ /**
54
+ * @name FairScaleError
55
+ * @description Thrown for any non-2xx response from a FairScale endpoint
56
+ * or for client-side validation failures (missing API key, invalid
57
+ * weights, etc.).
58
+ * @category Errors
59
+ * @since v0.11.0
60
+ * @extends SapError
61
+ */
62
+ export class FairScaleError extends SapError {
63
+ /** HTTP status from FairScale (0 for client-side errors). */
64
+ readonly status: number;
65
+ /** FairScale error code from the response body, if any. */
66
+ readonly upstreamCode?: string;
67
+
68
+ constructor(message: string, status: number, upstreamCode?: string) {
69
+ super(message, "FAIRSCALE_ERROR");
70
+ this.name = "FairScaleError";
71
+ this.status = status;
72
+ this.upstreamCode = upstreamCode;
73
+ Object.setPrototypeOf(this, new.target.prototype);
74
+ }
75
+ }
76
+
77
+ // ═══════════════════════════════════════════════════════════════════
78
+ // Constants — verified against docs.fairscale.xyz
79
+ // ═══════════════════════════════════════════════════════════════════
80
+
81
+ /**
82
+ * @name FAIRSCALE
83
+ * @description Public, documented constants for the FairScale platform.
84
+ * Verified against the docs at https://docs.fairscale.xyz on 2026-04-17.
85
+ * Use these instead of hard-coding strings — guarantees consistency across
86
+ * the SDK and any consumer code.
87
+ * @category Registries
88
+ * @since v0.11.0
89
+ */
90
+ export const FAIRSCALE = Object.freeze({
91
+ /** Agent & Credit API host. */
92
+ AGENT_API: "https://agent-api.fairscale.xyz",
93
+ /** Human Score API host. */
94
+ HUMAN_API: "https://api.fairscale.xyz",
95
+ /** Default request timeout matching the official SDK (10s). */
96
+ DEFAULT_TIMEOUT_MS: 10_000,
97
+ /** Server-side cache TTL on every endpoint (15 min). */
98
+ CACHE_TTL_SECONDS: 15 * 60,
99
+ /** Max wallets per `POST /v1/score/batch` request. */
100
+ BATCH_MAX_WALLETS: 25,
101
+ /** API key prefix. */
102
+ API_KEY_PREFIX: "zpka_",
103
+ /** Default `min_score` for `/v1/trust-gate`. */
104
+ DEFAULT_TRUST_GATE_MIN_SCORE: 40,
105
+
106
+ /** x402 micropayment metadata (Solana mainnet). */
107
+ X402: Object.freeze({
108
+ /** USDC mint on Solana mainnet. */
109
+ USDC_MINT: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
110
+ /** Wallet receiving x402 payments. */
111
+ PAY_TO: "fairAUEuR1SCcHL254Vb3F3XpUWLruJ2a11f6QfANEN",
112
+ /** Solana mainnet x402 network slug. */
113
+ NETWORK: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
114
+ /** Facilitator host. */
115
+ FACILITATOR: "https://x402.dexter.cash",
116
+ /** Price in USDC base units (micro-USDC) per agent / trust call. */
117
+ PRICE_AGENT_USDC_BASE: 5_000,
118
+ /** Price in USDC base units (micro-USDC) per credit assessment. */
119
+ PRICE_CREDIT_USDC_BASE: 500_000,
120
+ /** Default x402 settlement timeout (seconds). */
121
+ MAX_TIMEOUT_SECONDS: 60,
122
+ }),
123
+
124
+ /** Documented agent-tier ranges (inclusive). */
125
+ AGENT_TIER_RANGES: Object.freeze({
126
+ bronze: [0, 39],
127
+ silver: [40, 54],
128
+ gold: [55, 69],
129
+ platinum: [70, 84],
130
+ diamond: [85, 100],
131
+ } as const),
132
+
133
+ /** Documented credit `risk_band` ranges (inclusive). */
134
+ RISK_BAND_RANGES: Object.freeze({
135
+ decline: [0, 24],
136
+ deep_subprime: [25, 44],
137
+ subprime: [45, 59],
138
+ near_prime: [60, 74],
139
+ prime: [75, 100],
140
+ } as const),
141
+
142
+ /** Plan tiers — daily request quota / per-minute rate limit. */
143
+ PLAN_QUOTAS: Object.freeze({
144
+ free: { dailyRequests: 1_000, rpm: 10 },
145
+ builder: { dailyRequests: 20_000, rpm: 100 },
146
+ scale: { dailyRequests: 50_000, rpm: 300 },
147
+ pro: { dailyRequests: 100_000, rpm: 600 },
148
+ } as const),
149
+
150
+ /** Pillar weights for `/v1/score/ai` presets, exactly as documented. */
151
+ PRESET_WEIGHTS: Object.freeze({
152
+ default: { verification: 0.30, wallet_history: 0.25, work_history: 0.10, network_quality: 0.25, peer_reputation: 0.10 },
153
+ trust_focused: { verification: 0.50, wallet_history: 0.20, work_history: 0.10, network_quality: 0.10, peer_reputation: 0.10 },
154
+ work_focused: { verification: 0.20, wallet_history: 0.15, work_history: 0.40, network_quality: 0.15, peer_reputation: 0.10 },
155
+ defi: { verification: 0.25, wallet_history: 0.30, work_history: 0.10, network_quality: 0.25, peer_reputation: 0.10 },
156
+ hiring: { verification: 0.35, wallet_history: 0.15, work_history: 0.25, network_quality: 0.15, peer_reputation: 0.10 },
157
+ } as const),
158
+
159
+ /** Allowed sort fields for `/v1/directory` and `/v1/leaderboard`. */
160
+ DIRECTORY_SORT_FIELDS: [
161
+ "agent_fairscore",
162
+ "verification",
163
+ "wallet_history",
164
+ "work_history",
165
+ "network_quality",
166
+ "peer_reputation",
167
+ "reliability",
168
+ "track_record",
169
+ "economic_stake",
170
+ "ecosystem",
171
+ ] as const,
172
+
173
+ /** Documented machine-readable error codes. */
174
+ ERROR_CODES: [
175
+ "missing_wallet",
176
+ "invalid_wallet",
177
+ "invalid_preset",
178
+ "weights_must_sum_to_1",
179
+ "missing_weights",
180
+ "too_many_wallets",
181
+ "daily_limit_exceeded",
182
+ "upstream_error",
183
+ ] as const,
184
+ } as const);
185
+
186
+ // ═══════════════════════════════════════════════════════════════════
187
+ // Types — FairScale public API
188
+ // ═══════════════════════════════════════════════════════════════════
189
+
190
+ /**
191
+ * Agent / Human tier — documented ranges:
192
+ * `bronze 0–39 · silver 40–54 · gold 55–69 · platinum 70–84 · diamond 85–100`.
193
+ */
194
+ export type FairScaleTier =
195
+ | "bronze"
196
+ | "silver"
197
+ | "gold"
198
+ | "platinum"
199
+ | "diamond";
200
+
201
+ /** Built-in weight presets for `GET /v1/score/ai`. */
202
+ export type FairScalePreset = keyof typeof FAIRSCALE.PRESET_WEIGHTS;
203
+
204
+ /** Task profiles accepted by `/v1/score` and `/v1/trust-gate`. */
205
+ export type FairScaleTask =
206
+ | "defi_execution"
207
+ | "trust_focused"
208
+ | "work_focused"
209
+ | "hiring";
210
+
211
+ /**
212
+ * `recommendation` block returned by `/v1/score` — qualitative tier
213
+ * with a label and color hint for UI rendering.
214
+ */
215
+ export type FairScaleRecommendationTier =
216
+ | "trusted"
217
+ | "caution"
218
+ | "high_risk"
219
+ | "unverified";
220
+
221
+ /** Sort fields accepted by `/v1/directory` and `/v1/leaderboard`. */
222
+ export type FairScaleDirectorySort =
223
+ (typeof FAIRSCALE.DIRECTORY_SORT_FIELDS)[number];
224
+
225
+ /** Five agent-scoring pillars (0–100 each). */
226
+ export interface FairScalePillars {
227
+ readonly verification: number;
228
+ readonly wallet_history: number;
229
+ readonly work_history: number;
230
+ readonly network_quality: number;
231
+ readonly peer_reputation: number;
232
+ }
233
+
234
+ /** Behavioural badge emitted by both agent and human endpoints. */
235
+ export interface FairScaleBadge {
236
+ readonly id: string;
237
+ readonly label: string;
238
+ readonly description?: string;
239
+ readonly tier?: "bronze" | "silver" | "gold" | "platinum";
240
+ }
241
+
242
+ /** Recommended action returned by the human `/score` endpoint. */
243
+ export interface FairScaleAction {
244
+ readonly id: string;
245
+ readonly label: string;
246
+ readonly description: string;
247
+ readonly priority: "high" | "medium" | "low";
248
+ readonly cta: string;
249
+ }
250
+
251
+ /** Verification flags returned in `score.signals`. */
252
+ export interface FairScaleSignals {
253
+ readonly fairscore_base?: number;
254
+ readonly said_score?: number;
255
+ readonly said_trust_tier?: FairScaleTier;
256
+ readonly attestations?: number;
257
+ readonly is_registered?: boolean;
258
+ readonly is_verified?: boolean;
259
+ readonly is_said_agent?: boolean;
260
+ readonly is_erc8004?: boolean;
261
+ readonly [k: string]: unknown;
262
+ }
263
+
264
+ /** Description-alignment block in `/v1/score` response. */
265
+ export interface FairScaleDescriptionAlignment {
266
+ readonly bonus: number;
267
+ readonly label: "verified" | "partial" | "unverified" | string;
268
+ readonly matched: ReadonlyArray<string>;
269
+ readonly claimed: ReadonlyArray<string>;
270
+ }
271
+
272
+ /** Red flag returned in `/v1/score` response. */
273
+ export interface FairScaleRedFlag {
274
+ readonly type: string;
275
+ readonly reason: string;
276
+ readonly severity?: "critical" | "warning" | "info";
277
+ }
278
+
279
+ /** Verifications block in `/v1/score` response. */
280
+ export interface FairScaleVerifications {
281
+ readonly said_onchain?: boolean;
282
+ readonly erc8004?: boolean;
283
+ readonly sati?: boolean;
284
+ readonly liveness?: boolean;
285
+ readonly x402?: boolean;
286
+ readonly [k: string]: boolean | undefined;
287
+ }
288
+
289
+ /** Standard response meta envelope. */
290
+ export interface FairScaleMeta {
291
+ readonly scored_at?: string;
292
+ readonly from_cache?: boolean;
293
+ readonly cached?: boolean;
294
+ readonly provider: string;
295
+ readonly version?: string;
296
+ readonly layer?: string;
297
+ readonly latency_ms?: number;
298
+ readonly amount_assessed?: number;
299
+ }
300
+
301
+ /** Response of `GET /v1/score`. */
302
+ export interface AgentScoreResult {
303
+ readonly wallet: string;
304
+ readonly score: number;
305
+ readonly tier: FairScaleTier;
306
+ readonly recommendation?: {
307
+ readonly tier: FairScaleRecommendationTier;
308
+ readonly label: string;
309
+ readonly color: "green" | "yellow" | "red" | "gray" | string;
310
+ };
311
+ readonly pillars: FairScalePillars;
312
+ readonly signals?: FairScaleSignals;
313
+ readonly red_flags?: ReadonlyArray<FairScaleRedFlag>;
314
+ readonly badges?: ReadonlyArray<FairScaleBadge>;
315
+ readonly description_alignment?: FairScaleDescriptionAlignment;
316
+ readonly work_history_sources?: ReadonlyArray<string>;
317
+ readonly verifications?: FairScaleVerifications;
318
+ readonly meta?: FairScaleMeta;
319
+ /** Present only on per-wallet entries inside a batch response. */
320
+ readonly error?: string;
321
+ }
322
+
323
+ /** Response of `GET /v1/trust-gate`. */
324
+ export interface TrustGateResult {
325
+ readonly wallet: string;
326
+ readonly allowed: boolean;
327
+ readonly score: number;
328
+ readonly reason:
329
+ | "score_above_threshold"
330
+ | "score_below_threshold"
331
+ | "missing_verification"
332
+ | string;
333
+ readonly meta?: FairScaleMeta;
334
+ }
335
+
336
+ /** Response of `POST /v1/score/batch`. */
337
+ export interface BatchScoreResult {
338
+ readonly total: number;
339
+ readonly scored: number;
340
+ readonly results: ReadonlyArray<AgentScoreResult>;
341
+ readonly meta?: FairScaleMeta;
342
+ }
343
+
344
+ export interface ScoreOptions {
345
+ /** Apply a built-in scoring profile. */
346
+ readonly task?: FairScaleTask;
347
+ /** Override the API key for this call. */
348
+ readonly apiKey?: string;
349
+ /** Per-call timeout (ms). Defaults to client default. */
350
+ readonly timeoutMs?: number;
351
+ /** AbortSignal for cancellation. */
352
+ readonly signal?: AbortSignal;
353
+ }
354
+
355
+ export interface TrustGateOptions extends ScoreOptions {
356
+ /** Minimum score to pass (0–100). Default 40. */
357
+ readonly minScore?: number;
358
+ /** Require at least one registry verification. */
359
+ readonly requireVerification?: boolean;
360
+ }
361
+
362
+ export interface ScoreAiOptions extends ScoreOptions {
363
+ /** Use a built-in preset. Mutually exclusive with `weights`. */
364
+ readonly preset?: FairScalePreset;
365
+ /** Custom pillar weights — must sum to 1.0 ± 0.02. */
366
+ readonly weights?: FairScalePillars;
367
+ }
368
+
369
+ export interface DirectoryOptions extends ScoreOptions {
370
+ readonly page?: number;
371
+ /** Default 25, max 100. */
372
+ readonly limit?: number;
373
+ readonly sort?: FairScaleDirectorySort;
374
+ readonly minScore?: number;
375
+ readonly verifiedOnly?: boolean;
376
+ readonly recommendation?: FairScaleRecommendationTier;
377
+ readonly source?: "said" | "erc8004" | "sati";
378
+ readonly search?: string;
379
+ readonly hasAttestations?: boolean;
380
+ }
381
+
382
+ /** Single entry returned by `/v1/directory.results[]`. */
383
+ export interface DirectoryEntry {
384
+ readonly wallet: string;
385
+ readonly name?: string;
386
+ readonly description?: string;
387
+ readonly score: number;
388
+ readonly tier: FairScaleTier;
389
+ readonly pillars?: FairScalePillars;
390
+ readonly recommendation?: AgentScoreResult["recommendation"];
391
+ readonly verifications?: FairScaleVerifications;
392
+ readonly source?: "said" | "erc8004" | "sati";
393
+ readonly [k: string]: unknown;
394
+ }
395
+
396
+ /** Response of `GET /v1/directory`. */
397
+ export interface DirectoryResult {
398
+ readonly total: number;
399
+ readonly page: number;
400
+ readonly limit: number;
401
+ readonly results: ReadonlyArray<DirectoryEntry>;
402
+ readonly meta?: FairScaleMeta;
403
+ }
404
+
405
+ /** Response of `GET /v1/leaderboard`. */
406
+ export interface LeaderboardResult {
407
+ readonly metric: string;
408
+ readonly limit: number;
409
+ readonly results: ReadonlyArray<DirectoryEntry>;
410
+ readonly meta?: FairScaleMeta;
411
+ }
412
+
413
+ /** Response of `GET /v1/score-history`. */
414
+ export interface ScoreHistoryResult {
415
+ readonly wallet: string;
416
+ readonly history: ReadonlyArray<{
417
+ readonly scored_at: string;
418
+ readonly score: number;
419
+ readonly tier?: FairScaleTier;
420
+ }>;
421
+ readonly meta?: FairScaleMeta;
422
+ }
423
+
424
+ export interface CreditOptions extends ScoreOptions {
425
+ /** Loan amount in USD. Default 1000. */
426
+ readonly amount?: number;
427
+ /** Bypass 15-min cache. SDK accepts boolean — wire-format is `0|1`. */
428
+ readonly nocache?: boolean;
429
+ /** Optional social-proof header forwarded as `x-social-identity`. */
430
+ readonly socialIdentity?: string;
431
+ }
432
+
433
+ /** Lending terms inside the credit underwriting block. */
434
+ export interface CreditLendingTerms {
435
+ readonly recommendation: string;
436
+ readonly suggested_apr_range: { readonly low: number; readonly high: number };
437
+ readonly collateral_ratio: number;
438
+ readonly collateral_note: string;
439
+ readonly max_credit_line: number;
440
+ readonly max_term_days: number;
441
+ readonly identity_level:
442
+ | "kyc"
443
+ | "strong"
444
+ | "said"
445
+ | "matrica"
446
+ | "partial"
447
+ | "none";
448
+ readonly identity_enhanced: boolean;
449
+ }
450
+
451
+ /** Risk flag inside credit underwriting. */
452
+ export interface CreditRiskFlag {
453
+ readonly type: "critical" | "warning" | "positive";
454
+ readonly signal: string;
455
+ readonly detail: string;
456
+ }
457
+
458
+ /** Underwriting block. */
459
+ export interface CreditUnderwriting {
460
+ readonly opinion: string;
461
+ readonly lending_terms: CreditLendingTerms;
462
+ readonly risk_flags: ReadonlyArray<CreditRiskFlag>;
463
+ readonly data_confidence?: Record<string, unknown>;
464
+ }
465
+
466
+ /** Confidence block. */
467
+ export interface CreditConfidence {
468
+ readonly score: number;
469
+ readonly level: "high" | "medium" | "low";
470
+ readonly summary: string;
471
+ readonly limitations: ReadonlyArray<string>;
472
+ }
473
+
474
+ /** Five credit pillars. */
475
+ export interface CreditPillars {
476
+ readonly financial_position: { readonly score: number };
477
+ readonly credit_history: { readonly score: number };
478
+ readonly income_capacity: { readonly score: number };
479
+ readonly behavioural: { readonly score: number };
480
+ readonly identity_trust: { readonly score: number };
481
+ }
482
+
483
+ /** Attestation envelope (HMAC-SHA256 signed proof). */
484
+ export interface CreditAttestation {
485
+ readonly type: "signed_response" | string;
486
+ readonly payload_hash: string;
487
+ readonly payload_fields: string;
488
+ readonly note?: string;
489
+ }
490
+
491
+ /** Response of `GET /v1/credit`. */
492
+ export interface CreditResult {
493
+ readonly wallet: string;
494
+ readonly fairscore: number;
495
+ readonly fairscore_tier: FairScaleTier | "unverified";
496
+ readonly credit_score: number;
497
+ readonly risk_band:
498
+ | "prime"
499
+ | "near_prime"
500
+ | "subprime"
501
+ | "deep_subprime"
502
+ | "decline";
503
+ readonly confidence: CreditConfidence;
504
+ readonly underwriting: CreditUnderwriting;
505
+ readonly credit_pillars: CreditPillars;
506
+ readonly affordability?: Record<string, unknown>;
507
+ readonly trust_pillars?: Record<string, unknown>;
508
+ readonly credit_data?: Record<string, unknown>;
509
+ readonly flags?: Record<string, unknown>;
510
+ readonly attestation: CreditAttestation;
511
+ readonly meta?: FairScaleMeta;
512
+ }
513
+
514
+ // ── Human Score API ──────────────────────────────────────────────────
515
+
516
+ /**
517
+ * 15 on-chain features returned by `/score`. Field names and units match
518
+ * https://docs.fairscale.xyz/docs/api-score#features exactly.
519
+ */
520
+ export interface HumanScoreFeatures {
521
+ // Portfolio Composition
522
+ readonly native_sol_percentile: number;
523
+ readonly major_percentile_score: number;
524
+ readonly stable_percentile_score: number;
525
+ readonly lst_percentile_score: number;
526
+ // Capital Flow
527
+ readonly net_sol_flow_30d: number;
528
+ // Holding Conviction
529
+ readonly median_hold_days: number;
530
+ readonly conviction_ratio: number;
531
+ readonly no_instant_dumps: 0 | 1;
532
+ // Activity Tempo
533
+ readonly tx_count: number;
534
+ readonly active_days: number;
535
+ readonly median_gap_hours: number;
536
+ // Trading Behaviour
537
+ readonly tempo_cv: number;
538
+ readonly burst_ratio: number;
539
+ // Breadth
540
+ readonly platform_diversity: number;
541
+ readonly wallet_age_score: number;
542
+ }
543
+
544
+ /** Response of `GET /score` (Human Score API). */
545
+ export interface HumanScoreResult {
546
+ readonly wallet: string;
547
+ /** Final blended score (0–100) — `0.50·base + 0.20·social + 0.30·peer`. */
548
+ readonly fairscore: number;
549
+ /** Raw on-chain neural-network score (features only). */
550
+ readonly fairscore_base: number;
551
+ /** Social reputation (0 if no X handle linked). */
552
+ readonly social_score: number;
553
+ /** Peer-vouch score (0 if no vouches received). */
554
+ readonly peer_score: number;
555
+ readonly verified_human: boolean;
556
+ readonly tier: FairScaleTier;
557
+ readonly badges: ReadonlyArray<FairScaleBadge>;
558
+ readonly actions: ReadonlyArray<FairScaleAction>;
559
+ readonly features: HumanScoreFeatures;
560
+ /** Present only on cache-hit responses. */
561
+ readonly cached?: boolean;
562
+ readonly timestamp: string;
563
+ }
564
+
565
+
566
+ // ═══════════════════════════════════════════════════════════════════
567
+ // Types — Aggregation (SAP + FairScale)
568
+ // ═══════════════════════════════════════════════════════════════════
569
+
570
+ export interface AggregatedReputation {
571
+ /** Agent wallet. */
572
+ readonly wallet: string;
573
+ /** SAP on-chain reputation snapshot (null if agent not registered). */
574
+ readonly sap: {
575
+ readonly registered: boolean;
576
+ /** 0–100 SAP `reputationScore` (null if no feedback yet). */
577
+ readonly score: number | null;
578
+ readonly totalFeedbacks: number;
579
+ readonly totalCallsServed: string;
580
+ readonly isActive: boolean;
581
+ };
582
+ /** FairScale snapshot (null if FairScale lookup failed). */
583
+ readonly fairscale: AgentScoreResult | null;
584
+ /** Final blended signal. */
585
+ readonly combined: {
586
+ /** 0–100 weighted score. */
587
+ readonly score: number;
588
+ /** Bucket derived from `score`: low <40, medium <60, high <80, elite ≥80. */
589
+ readonly tier: "low" | "medium" | "high" | "elite";
590
+ /**
591
+ * Confidence in the blended signal (0–1). Penalises:
592
+ * - Missing source (only one of the two responded)
593
+ * - Low SAP feedback count
594
+ * - FairScale `from_cache: true`
595
+ */
596
+ readonly confidence: number;
597
+ /** Effective weights actually applied (after missing-source rebalance). */
598
+ readonly weights: { sap: number; fairscale: number };
599
+ /** Reasons that affected the score (red flags, gates, etc.). */
600
+ readonly notes: ReadonlyArray<string>;
601
+ };
602
+ readonly meta: {
603
+ readonly provider: "SAP+FairScale";
604
+ readonly computedAt: string;
605
+ };
606
+ }
607
+
608
+ export interface AggregateOptions extends ScoreOptions {
609
+ /**
610
+ * Weights applied to each source. Defaults to `{ sap: 0.5, fairscale: 0.5 }`.
611
+ * Must sum to 1.0 ± 0.01. If one source is missing, the present source's
612
+ * weight is renormalised to 1.0 (and `confidence` is reduced).
613
+ */
614
+ readonly weights?: { sap: number; fairscale: number };
615
+ /** Minimum SAP feedbacks required for SAP to count (else SAP weight → 0). */
616
+ readonly require?: { sapMinFeedbacks?: number };
617
+ /**
618
+ * If true, throws when both sources are unavailable. Default `false`
619
+ * (returns `combined.score = 0`, `confidence = 0`).
620
+ */
621
+ readonly strict?: boolean;
622
+ }
623
+
624
+ // ═══════════════════════════════════════════════════════════════════
625
+ // Configuration
626
+ // ═══════════════════════════════════════════════════════════════════
627
+
628
+ export interface FairScaleConfig {
629
+ /** API key (or read from env `FAIRSCALE_API_KEY`). */
630
+ readonly apiKey?: string;
631
+ /** Override agent-api base URL. */
632
+ readonly baseUrl?: string;
633
+ /** Override human-api base URL. */
634
+ readonly humanBaseUrl?: string;
635
+ /** Default request timeout (ms). Default 10_000. */
636
+ readonly timeoutMs?: number;
637
+ /** Custom fetch implementation (for tests / Edge proxies). */
638
+ readonly fetch?: typeof fetch;
639
+ }
640
+
641
+ const DEFAULT_BASE_URL = "https://agent-api.fairscale.xyz";
642
+ const DEFAULT_HUMAN_BASE_URL = "https://api.fairscale.xyz";
643
+ const DEFAULT_TIMEOUT_MS = 10_000;
644
+
645
+ // ═══════════════════════════════════════════════════════════════════
646
+ // Registry
647
+ // ═══════════════════════════════════════════════════════════════════
648
+
649
+ /**
650
+ * @name FairScaleRegistry
651
+ * @description High-level FairScale client + SAP reputation aggregator.
652
+ * Exposed lazily as `client.fairscale`.
653
+ * @category Registries
654
+ * @since v0.11.0
655
+ */
656
+ export class FairScaleRegistry {
657
+ readonly #program: SapProgram;
658
+ readonly #apiKey: string | undefined;
659
+ readonly #baseUrl: string;
660
+ readonly #humanBaseUrl: string;
661
+ readonly #timeoutMs: number;
662
+ readonly #fetch: typeof fetch;
663
+
664
+ #discovery?: DiscoveryRegistry;
665
+ #human?: HumanScoreNamespace;
666
+
667
+ constructor(program: SapProgram, config: FairScaleConfig = {}) {
668
+ this.#program = program;
669
+ this.#apiKey =
670
+ config.apiKey ??
671
+ (typeof process !== "undefined"
672
+ ? process.env?.FAIRSCALE_API_KEY
673
+ : undefined);
674
+ this.#baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
675
+ this.#humanBaseUrl = (config.humanBaseUrl ?? DEFAULT_HUMAN_BASE_URL).replace(
676
+ /\/+$/,
677
+ "",
678
+ );
679
+ this.#timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
680
+ if (config.fetch) {
681
+ this.#fetch = config.fetch;
682
+ } else if (typeof fetch !== "undefined") {
683
+ this.#fetch = fetch.bind(globalThis);
684
+ } else {
685
+ throw new FairScaleError(
686
+ "global `fetch` not available — pass `config.fetch`",
687
+ 0,
688
+ "no_fetch",
689
+ );
690
+ }
691
+ }
692
+
693
+ /** Lazy DiscoveryRegistry, used by `aggregate()` to read on-chain SAP state. */
694
+ get #disc(): DiscoveryRegistry {
695
+ return (this.#discovery ??= new DiscoveryRegistry(this.#program));
696
+ }
697
+
698
+ /** Human Score API namespace (`client.fairscale.human.*`). */
699
+ get human(): HumanScoreNamespace {
700
+ return (this.#human ??= new HumanScoreNamespace(
701
+ this.#humanBaseUrl,
702
+ this.#apiKey,
703
+ this.#timeoutMs,
704
+ this.#fetch,
705
+ ));
706
+ }
707
+
708
+ // ── Agent & Credit API ────────────────────────────────────────────
709
+
710
+ /**
711
+ * @description `GET /v1/score` — composite trust score.
712
+ */
713
+ score(agent: PublicKey | string, opts: ScoreOptions = {}): Promise<AgentScoreResult> {
714
+ const wallet = toWallet(agent);
715
+ const url = this.#url("/v1/score", { wallet, task: opts.task });
716
+ return this.#getJson<AgentScoreResult>(url, opts);
717
+ }
718
+
719
+ /**
720
+ * @description `GET /v1/trust-gate` — binary allow/deny.
721
+ */
722
+ trustGate(
723
+ agent: PublicKey | string,
724
+ opts: TrustGateOptions = {},
725
+ ): Promise<TrustGateResult> {
726
+ const wallet = toWallet(agent);
727
+ const url = this.#url("/v1/trust-gate", {
728
+ wallet,
729
+ task: opts.task,
730
+ min_score: opts.minScore,
731
+ require_verification: opts.requireVerification,
732
+ });
733
+ return this.#getJson<TrustGateResult>(url, opts);
734
+ }
735
+
736
+ /**
737
+ * @description `POST /v1/score/batch` — up to 25 wallets per call.
738
+ * Splits larger inputs into chunks of 25 and merges results.
739
+ */
740
+ async scoreBatch(
741
+ agents: ReadonlyArray<PublicKey | string>,
742
+ opts: ScoreOptions = {},
743
+ ): Promise<BatchScoreResult> {
744
+ const wallets = agents.map(toWallet);
745
+ if (wallets.length === 0) {
746
+ return { total: 0, scored: 0, results: [] };
747
+ }
748
+ const chunks: string[][] = [];
749
+ for (let i = 0; i < wallets.length; i += 25) {
750
+ chunks.push(wallets.slice(i, i + 25));
751
+ }
752
+ const responses = await Promise.all(
753
+ chunks.map((chunk) =>
754
+ this.#postJson<BatchScoreResult>(this.#url("/v1/score/batch"), opts, {
755
+ wallets: chunk,
756
+ task: opts.task,
757
+ }),
758
+ ),
759
+ );
760
+ const merged: BatchScoreResult = {
761
+ total: wallets.length,
762
+ scored: responses.reduce((acc, r) => acc + r.scored, 0),
763
+ results: responses.flatMap((r) => r.results),
764
+ meta: responses[0]?.meta,
765
+ };
766
+ return merged;
767
+ }
768
+
769
+ /**
770
+ * @description `GET /v1/score/ai` — composable score with preset or custom weights.
771
+ */
772
+ scoreAI(
773
+ agent: PublicKey | string,
774
+ opts: ScoreAiOptions,
775
+ ): Promise<AgentScoreResult> {
776
+ if (!opts.preset && !opts.weights) {
777
+ throw new FairScaleError(
778
+ "scoreAI requires either `preset` or `weights`",
779
+ 0,
780
+ "missing_preset_or_weights",
781
+ );
782
+ }
783
+ if (opts.weights) {
784
+ const sum = (Object.values(opts.weights) as number[]).reduce(
785
+ (a, b) => a + (b ?? 0),
786
+ 0,
787
+ );
788
+ if (Math.abs(sum - 1) > 0.02) {
789
+ throw new FairScaleError(
790
+ `custom weights must sum to 1.0 (±0.02), got ${sum}`,
791
+ 0,
792
+ "weights_must_sum_to_1",
793
+ );
794
+ }
795
+ }
796
+ const url = this.#url("/v1/score/ai", {
797
+ wallet: toWallet(agent),
798
+ preset: opts.preset,
799
+ ...(opts.weights ?? {}),
800
+ });
801
+ return this.#getJson<AgentScoreResult>(url, opts);
802
+ }
803
+
804
+ /**
805
+ * @description `GET /v1/agent` — full agent profile (registry details + scoring data).
806
+ */
807
+ agentProfile(
808
+ agent: PublicKey | string,
809
+ opts: ScoreOptions = {},
810
+ ): Promise<AgentScoreResult & { profile?: Record<string, unknown> }> {
811
+ const url = this.#url("/v1/agent", { wallet: toWallet(agent) });
812
+ return this.#getJson<
813
+ AgentScoreResult & { profile?: Record<string, unknown> }
814
+ >(url, opts);
815
+ }
816
+
817
+ /**
818
+ * @description `GET /v1/score-history` — score trend over time.
819
+ */
820
+ scoreHistory(
821
+ agent: PublicKey | string,
822
+ opts: ScoreOptions = {},
823
+ ): Promise<ScoreHistoryResult> {
824
+ const url = this.#url("/v1/score-history", { wallet: toWallet(agent) });
825
+ return this.#getJson<ScoreHistoryResult>(url, opts);
826
+ }
827
+
828
+ /**
829
+ * @description `GET /v1/directory` — query the indexed agent directory.
830
+ */
831
+ directory(opts: DirectoryOptions = {}): Promise<DirectoryResult> {
832
+ const url = this.#url("/v1/directory", {
833
+ page: opts.page,
834
+ limit: opts.limit,
835
+ sort: opts.sort,
836
+ min_score: opts.minScore,
837
+ verified_only: opts.verifiedOnly,
838
+ recommendation: opts.recommendation,
839
+ source: opts.source,
840
+ search: opts.search,
841
+ has_attestations: opts.hasAttestations,
842
+ });
843
+ return this.#getJson<DirectoryResult>(url, opts);
844
+ }
845
+
846
+ /**
847
+ * @description `GET /v1/leaderboard` — top-scoring agents by metric.
848
+ */
849
+ leaderboard(opts: {
850
+ metric?: FairScaleDirectorySort;
851
+ limit?: number;
852
+ apiKey?: string;
853
+ timeoutMs?: number;
854
+ signal?: AbortSignal;
855
+ } = {}): Promise<LeaderboardResult> {
856
+ const url = this.#url("/v1/leaderboard", {
857
+ metric: opts.metric,
858
+ limit: opts.limit,
859
+ });
860
+ return this.#getJson<LeaderboardResult>(url, opts);
861
+ }
862
+
863
+ /**
864
+ * @description `GET /v1/credit` — full credit assessment ($0.50 USDC per call).
865
+ * Uses `X-Api-Key` header instead of `fairkey`. Wire-format for `nocache`
866
+ * is `0|1` per the docs; the SDK accepts a boolean and converts it.
867
+ */
868
+ credit(
869
+ agent: PublicKey | string,
870
+ opts: CreditOptions = {},
871
+ ): Promise<CreditResult> {
872
+ const url = this.#url("/v1/credit", {
873
+ wallet: toWallet(agent),
874
+ amount: opts.amount,
875
+ nocache: opts.nocache === undefined ? undefined : opts.nocache ? 1 : 0,
876
+ });
877
+ return this.#getJson<CreditResult>(url, opts, {
878
+ authHeader: "X-Api-Key",
879
+ extraHeaders: opts.socialIdentity
880
+ ? { "x-social-identity": opts.socialIdentity }
881
+ : undefined,
882
+ });
883
+ }
884
+
885
+ // ── Aggregation (SAP + FairScale) ─────────────────────────────────
886
+
887
+ /**
888
+ * @description Merge SAP on-chain reputation with FairScale into a single
889
+ * weighted signal. Falls back gracefully if either source is unavailable.
890
+ *
891
+ * @param agentWallet - The agent's owner wallet (NOT the agent PDA).
892
+ * @param opts - Weights, gating rules, and per-call overrides.
893
+ * @returns {Promise<AggregatedReputation>}
894
+ *
895
+ * @example
896
+ * ```ts
897
+ * const r = await client.fairscale.aggregate(agentWallet, {
898
+ * weights: { sap: 0.4, fairscale: 0.6 },
899
+ * require: { sapMinFeedbacks: 2 },
900
+ * });
901
+ * if (r.combined.tier === "high" || r.combined.tier === "elite") accept();
902
+ * ```
903
+ */
904
+ async aggregate(
905
+ agentWallet: PublicKey,
906
+ opts: AggregateOptions = {},
907
+ ): Promise<AggregatedReputation> {
908
+ const w = opts.weights ?? { sap: 0.5, fairscale: 0.5 };
909
+ if (Math.abs(w.sap + w.fairscale - 1) > 0.01) {
910
+ throw new FairScaleError(
911
+ `aggregate weights must sum to 1.0 (±0.01), got ${w.sap + w.fairscale}`,
912
+ 0,
913
+ "weights_must_sum_to_1",
914
+ );
915
+ }
916
+ const minFeedbacks = opts.require?.sapMinFeedbacks ?? 0;
917
+
918
+ const [sapProfile, fsScore] = await Promise.allSettled([
919
+ this.#disc.getAgentProfile(agentWallet),
920
+ this.score(agentWallet, opts),
921
+ ]);
922
+
923
+ const notes: string[] = [];
924
+
925
+ // ── SAP slice ───────────────────────────────────────────────
926
+ const sapResult = sapProfile.status === "fulfilled" ? sapProfile.value : null;
927
+ const sapScore = extractSapScore(sapResult, minFeedbacks, notes);
928
+
929
+ // ── FairScale slice ─────────────────────────────────────────
930
+ const fairscale =
931
+ fsScore.status === "fulfilled" ? fsScore.value : null;
932
+ if (fsScore.status === "rejected") {
933
+ notes.push(
934
+ `fairscale_unavailable:${(fsScore.reason as Error)?.message ?? "unknown"}`,
935
+ );
936
+ }
937
+ if (fairscale?.red_flags?.length) {
938
+ notes.push(`red_flags:${fairscale.red_flags.length}`);
939
+ }
940
+ if (fairscale?.meta?.from_cache) notes.push("fairscale_cache_hit");
941
+
942
+ // ── Blend ───────────────────────────────────────────────────
943
+ const sapAvail = sapScore !== null ? w.sap : 0;
944
+ const fsAvail = fairscale ? w.fairscale : 0;
945
+ const totalW = sapAvail + fsAvail;
946
+
947
+ let combinedScore = 0;
948
+ let weights = { sap: 0, fairscale: 0 };
949
+ if (totalW === 0) {
950
+ if (opts.strict) {
951
+ throw new FairScaleError(
952
+ "no reputation source available (strict mode)",
953
+ 0,
954
+ "no_source",
955
+ );
956
+ }
957
+ notes.push("no_source");
958
+ } else {
959
+ const sapNorm = sapAvail / totalW;
960
+ const fsNorm = fsAvail / totalW;
961
+ combinedScore =
962
+ (sapScore ?? 0) * sapNorm + (fairscale?.score ?? 0) * fsNorm;
963
+ weights = { sap: round2(sapNorm), fairscale: round2(fsNorm) };
964
+ }
965
+
966
+ // ── Confidence ──────────────────────────────────────────────
967
+ let confidence = 1;
968
+ if (sapScore === null) confidence -= 0.35;
969
+ if (!fairscale) confidence -= 0.35;
970
+ if (sapResult && sapResult.identity.totalFeedbacks < 3) confidence -= 0.1;
971
+ if (fairscale?.meta?.from_cache) confidence -= 0.05;
972
+ if (fairscale?.red_flags?.length) {
973
+ confidence -= Math.min(0.2, fairscale.red_flags.length * 0.05);
974
+ }
975
+ confidence = Math.max(0, Math.min(1, confidence));
976
+
977
+ return {
978
+ wallet: agentWallet.toBase58(),
979
+ sap: {
980
+ registered: sapResult !== null,
981
+ score: sapScore,
982
+ totalFeedbacks: sapResult?.identity.totalFeedbacks ?? 0,
983
+ totalCallsServed:
984
+ sapResult?.identity.totalCallsServed.toString() ?? "0",
985
+ isActive: sapResult?.identity.isActive ?? false,
986
+ },
987
+ fairscale,
988
+ combined: {
989
+ score: round2(Math.max(0, Math.min(100, combinedScore))),
990
+ tier: bucketTier(combinedScore),
991
+ confidence: round2(confidence),
992
+ weights,
993
+ notes,
994
+ },
995
+ meta: {
996
+ provider: "SAP+FairScale",
997
+ computedAt: new Date().toISOString(),
998
+ },
999
+ };
1000
+ }
1001
+
1002
+ // ── HTTP plumbing ─────────────────────────────────────────────────
1003
+
1004
+ #url(path: string, params?: Record<string, unknown>): string {
1005
+ const u = new URL(this.#baseUrl + path);
1006
+ if (params) {
1007
+ for (const [k, v] of Object.entries(params)) {
1008
+ if (v === undefined || v === null) continue;
1009
+ u.searchParams.set(k, String(v));
1010
+ }
1011
+ }
1012
+ return u.toString();
1013
+ }
1014
+
1015
+ async #getJson<T>(
1016
+ url: string,
1017
+ opts: ScoreOptions,
1018
+ extra: {
1019
+ authHeader?: "fairkey" | "X-Api-Key";
1020
+ extraHeaders?: Record<string, string>;
1021
+ } = {},
1022
+ ): Promise<T> {
1023
+ return this.#request<T>(
1024
+ url,
1025
+ "GET",
1026
+ undefined,
1027
+ opts,
1028
+ extra.authHeader,
1029
+ extra.extraHeaders,
1030
+ );
1031
+ }
1032
+
1033
+ async #postJson<T>(
1034
+ url: string,
1035
+ opts: ScoreOptions,
1036
+ body: unknown,
1037
+ ): Promise<T> {
1038
+ return this.#request<T>(url, "POST", body, opts);
1039
+ }
1040
+
1041
+ async #request<T>(
1042
+ url: string,
1043
+ method: "GET" | "POST",
1044
+ body: unknown,
1045
+ opts: ScoreOptions,
1046
+ authHeader: "fairkey" | "X-Api-Key" = "fairkey",
1047
+ extraHeaders?: Record<string, string>,
1048
+ ): Promise<T> {
1049
+ const apiKey = opts.apiKey ?? this.#apiKey;
1050
+ const headers: Record<string, string> = { Accept: "application/json" };
1051
+ if (apiKey) headers[authHeader] = apiKey;
1052
+ if (body !== undefined) headers["Content-Type"] = "application/json";
1053
+ if (extraHeaders) Object.assign(headers, extraHeaders);
1054
+
1055
+ const ctrl = new AbortController();
1056
+ const timeoutMs = opts.timeoutMs ?? this.#timeoutMs;
1057
+ const timeout = setTimeout(() => ctrl.abort(), timeoutMs);
1058
+ if (opts.signal) {
1059
+ if (opts.signal.aborted) ctrl.abort();
1060
+ else opts.signal.addEventListener("abort", () => ctrl.abort(), { once: true });
1061
+ }
1062
+
1063
+ let res: Response;
1064
+ try {
1065
+ res = await this.#fetch(url, {
1066
+ method,
1067
+ headers,
1068
+ body: body !== undefined ? JSON.stringify(body) : undefined,
1069
+ signal: ctrl.signal,
1070
+ });
1071
+ } catch (err) {
1072
+ throw new FairScaleError(
1073
+ `network error: ${(err as Error).message}`,
1074
+ 0,
1075
+ "network_error",
1076
+ );
1077
+ } finally {
1078
+ clearTimeout(timeout);
1079
+ }
1080
+
1081
+ const text = await res.text();
1082
+ let json: unknown;
1083
+ try {
1084
+ json = text ? JSON.parse(text) : {};
1085
+ } catch {
1086
+ throw new FairScaleError(
1087
+ `invalid JSON from FairScale (${res.status})`,
1088
+ res.status,
1089
+ "invalid_json",
1090
+ );
1091
+ }
1092
+
1093
+ if (!res.ok) {
1094
+ const errCode =
1095
+ (json as { code?: string; error?: string })?.code ??
1096
+ (json as { error?: string })?.error ??
1097
+ `http_${res.status}`;
1098
+ throw new FairScaleError(
1099
+ `FairScale ${method} ${url} → ${res.status}: ${errCode}`,
1100
+ res.status,
1101
+ errCode,
1102
+ );
1103
+ }
1104
+
1105
+ return json as T;
1106
+ }
1107
+ }
1108
+
1109
+ // ═══════════════════════════════════════════════════════════════════
1110
+ // Human Score namespace
1111
+ // ═══════════════════════════════════════════════════════════════════
1112
+
1113
+ /**
1114
+ * @name HumanScoreNamespace
1115
+ * @description Wraps `api.fairscale.xyz` (Human Score API). Accessed via
1116
+ * `client.fairscale.human`.
1117
+ * @category Registries
1118
+ * @since v0.11.0
1119
+ */
1120
+ export class HumanScoreNamespace {
1121
+ readonly #baseUrl: string;
1122
+ readonly #apiKey: string | undefined;
1123
+ readonly #timeoutMs: number;
1124
+ readonly #fetch: typeof fetch;
1125
+
1126
+ constructor(
1127
+ baseUrl: string,
1128
+ apiKey: string | undefined,
1129
+ timeoutMs: number,
1130
+ fetchImpl: typeof fetch,
1131
+ ) {
1132
+ this.#baseUrl = baseUrl;
1133
+ this.#apiKey = apiKey;
1134
+ this.#timeoutMs = timeoutMs;
1135
+ this.#fetch = fetchImpl;
1136
+ }
1137
+
1138
+ /**
1139
+ * @description `GET /score` — full human wallet analysis.
1140
+ */
1141
+ score(
1142
+ wallet: PublicKey | string,
1143
+ opts: { twitter?: string; nocache?: boolean } & ScoreOptions = {},
1144
+ ): Promise<HumanScoreResult> {
1145
+ return this.#get<HumanScoreResult>(
1146
+ "/score",
1147
+ { wallet: toWallet(wallet), twitter: opts.twitter, nocache: opts.nocache },
1148
+ opts,
1149
+ );
1150
+ }
1151
+
1152
+ /** `GET /fairScore` — blended 0–1000 integer. */
1153
+ fairScoreOnly(
1154
+ wallet: PublicKey | string,
1155
+ opts: ScoreOptions = {},
1156
+ ): Promise<{ fair_score: number }> {
1157
+ return this.#get("/fairScore", { wallet: toWallet(wallet) }, opts);
1158
+ }
1159
+
1160
+ /** `GET /walletScore` — on-chain only 0–1000 integer. */
1161
+ walletScoreOnly(
1162
+ wallet: PublicKey | string,
1163
+ opts: ScoreOptions = {},
1164
+ ): Promise<{ wallet_score: number }> {
1165
+ return this.#get("/walletScore", { wallet: toWallet(wallet) }, opts);
1166
+ }
1167
+
1168
+ /** `GET /socialScore` — social only 0–1000 integer. */
1169
+ socialScoreOnly(
1170
+ wallet: PublicKey | string,
1171
+ opts: { twitter?: string } & ScoreOptions = {},
1172
+ ): Promise<{ social_score: number }> {
1173
+ return this.#get(
1174
+ "/socialScore",
1175
+ { wallet: toWallet(wallet), twitter: opts.twitter },
1176
+ opts,
1177
+ );
1178
+ }
1179
+
1180
+ async #get<T>(
1181
+ path: string,
1182
+ params: Record<string, unknown>,
1183
+ opts: ScoreOptions,
1184
+ ): Promise<T> {
1185
+ const url = new URL(this.#baseUrl + path);
1186
+ for (const [k, v] of Object.entries(params)) {
1187
+ if (v === undefined || v === null) continue;
1188
+ url.searchParams.set(k, String(v));
1189
+ }
1190
+ const apiKey = opts.apiKey ?? this.#apiKey;
1191
+ const headers: Record<string, string> = { Accept: "application/json" };
1192
+ if (apiKey) headers["fairkey"] = apiKey;
1193
+
1194
+ const ctrl = new AbortController();
1195
+ const timeout = setTimeout(() => ctrl.abort(), opts.timeoutMs ?? this.#timeoutMs);
1196
+ if (opts.signal) {
1197
+ if (opts.signal.aborted) ctrl.abort();
1198
+ else opts.signal.addEventListener("abort", () => ctrl.abort(), { once: true });
1199
+ }
1200
+
1201
+ let res: Response;
1202
+ try {
1203
+ res = await this.#fetch(url.toString(), { method: "GET", headers, signal: ctrl.signal });
1204
+ } catch (err) {
1205
+ throw new FairScaleError(
1206
+ `network error: ${(err as Error).message}`,
1207
+ 0,
1208
+ "network_error",
1209
+ );
1210
+ } finally {
1211
+ clearTimeout(timeout);
1212
+ }
1213
+ const text = await res.text();
1214
+ let json: unknown;
1215
+ try {
1216
+ json = text ? JSON.parse(text) : {};
1217
+ } catch {
1218
+ throw new FairScaleError(
1219
+ `invalid JSON from FairScale Human API (${res.status})`,
1220
+ res.status,
1221
+ "invalid_json",
1222
+ );
1223
+ }
1224
+ if (!res.ok) {
1225
+ const code =
1226
+ (json as { code?: string; error?: string })?.code ??
1227
+ (json as { error?: string })?.error ??
1228
+ `http_${res.status}`;
1229
+ throw new FairScaleError(
1230
+ `FairScale Human GET ${path} → ${res.status}: ${code}`,
1231
+ res.status,
1232
+ code,
1233
+ );
1234
+ }
1235
+ return json as T;
1236
+ }
1237
+ }
1238
+
1239
+ // ═══════════════════════════════════════════════════════════════════
1240
+ // Helpers
1241
+ // ═══════════════════════════════════════════════════════════════════
1242
+
1243
+ function toWallet(input: PublicKey | string): string {
1244
+ return typeof input === "string" ? input : input.toBase58();
1245
+ }
1246
+
1247
+ function round2(n: number): number {
1248
+ return Math.round(n * 100) / 100;
1249
+ }
1250
+
1251
+ function bucketTier(score: number): "low" | "medium" | "high" | "elite" {
1252
+ if (score >= 80) return "elite";
1253
+ if (score >= 60) return "high";
1254
+ if (score >= 40) return "medium";
1255
+ return "low";
1256
+ }
1257
+
1258
+ function extractSapScore(
1259
+ profile: AgentProfile | null,
1260
+ minFeedbacks: number,
1261
+ notes: string[],
1262
+ ): number | null {
1263
+ if (!profile) {
1264
+ notes.push("sap_unregistered");
1265
+ return null;
1266
+ }
1267
+ const fb = profile.identity.totalFeedbacks;
1268
+ if (fb < minFeedbacks) {
1269
+ notes.push(`sap_below_min_feedbacks(${fb}/${minFeedbacks})`);
1270
+ return null;
1271
+ }
1272
+ if (fb === 0) {
1273
+ notes.push("sap_no_feedback_yet");
1274
+ return null;
1275
+ }
1276
+ if (!profile.identity.isActive) notes.push("sap_inactive");
1277
+ return profile.identity.reputationScore;
1278
+ }