@agentkarma/sdk 0.1.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/LICENSE +21 -0
- package/README.md +216 -0
- package/dist/client.d.ts +64 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +323 -0
- package/dist/client.js.map +1 -0
- package/dist/errors.d.ts +64 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +78 -0
- package/dist/errors.js.map +1 -0
- package/dist/feedback.d.ts +31 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/feedback.js +35 -0
- package/dist/feedback.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/policy.d.ts +105 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +150 -0
- package/dist/policy.js.map +1 -0
- package/dist/types.d.ts +347 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -0
- package/src/client.ts +519 -0
- package/src/errors.ts +87 -0
- package/src/feedback.ts +51 -0
- package/src/index.ts +84 -0
- package/src/policy.ts +286 -0
- package/src/types.ts +406 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentKarma typed HTTP client.
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic. Works in Node 18+, Bun, Deno, browsers, edge runtimes —
|
|
5
|
+
* anywhere global `fetch` is available (or you provide one).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
AgentKarmaError,
|
|
10
|
+
AgentKarmaMalformedResponseError,
|
|
11
|
+
AgentKarmaNetworkError,
|
|
12
|
+
AgentKarmaNotFoundError,
|
|
13
|
+
AgentKarmaRateLimitError,
|
|
14
|
+
AgentKarmaServerError,
|
|
15
|
+
AgentKarmaTimeoutError,
|
|
16
|
+
AgentKarmaValidationError,
|
|
17
|
+
} from './errors.js';
|
|
18
|
+
import type {
|
|
19
|
+
AgentHistoryResponse,
|
|
20
|
+
BondBlock,
|
|
21
|
+
BondResponse,
|
|
22
|
+
CeloAgentSnapshot,
|
|
23
|
+
Chain,
|
|
24
|
+
ClientConfig,
|
|
25
|
+
FeedbackSubmission,
|
|
26
|
+
FeedbackSubmissionResponse,
|
|
27
|
+
FeedbackSummary,
|
|
28
|
+
FetchLike,
|
|
29
|
+
KarmaFace,
|
|
30
|
+
KarmaFaceData,
|
|
31
|
+
KarmaSnapshot,
|
|
32
|
+
RequestOptions,
|
|
33
|
+
SearchResponse,
|
|
34
|
+
SuccessionResponse,
|
|
35
|
+
SuccessionView,
|
|
36
|
+
SuretyView,
|
|
37
|
+
} from './types.js';
|
|
38
|
+
|
|
39
|
+
const SDK_VERSION = '0.1.0';
|
|
40
|
+
const DEFAULT_BASE_URL = 'https://agentkarma.io';
|
|
41
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
42
|
+
|
|
43
|
+
const SOLANA_ADDRESS_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
44
|
+
const SUPPORTED_CHAINS: readonly Chain[] = ['solana', 'celo', 'stellar', 'arc'];
|
|
45
|
+
|
|
46
|
+
interface InternalConfig {
|
|
47
|
+
baseUrl: string;
|
|
48
|
+
fetchImpl: FetchLike;
|
|
49
|
+
timeout: number;
|
|
50
|
+
headers: Record<string, string>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeConfig(input?: ClientConfig): InternalConfig {
|
|
54
|
+
const baseUrl = (input?.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
|
|
55
|
+
const userAgent = input?.userAgent ?? `@agentkarma/sdk/${SDK_VERSION}`;
|
|
56
|
+
return {
|
|
57
|
+
baseUrl,
|
|
58
|
+
fetchImpl: input?.fetch ?? globalThis.fetch,
|
|
59
|
+
timeout: input?.timeout ?? DEFAULT_TIMEOUT_MS,
|
|
60
|
+
headers: {
|
|
61
|
+
'User-Agent': userAgent,
|
|
62
|
+
Accept: 'application/json',
|
|
63
|
+
...(input?.headers ?? {}),
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildSignal(
|
|
69
|
+
configTimeout: number,
|
|
70
|
+
opts: RequestOptions | undefined,
|
|
71
|
+
): {
|
|
72
|
+
signal: AbortSignal;
|
|
73
|
+
timeoutSignal: AbortSignal;
|
|
74
|
+
timeoutMs: number;
|
|
75
|
+
cleanup: () => void;
|
|
76
|
+
} {
|
|
77
|
+
const timeoutMs = opts?.timeout ?? configTimeout;
|
|
78
|
+
const timeoutController = new AbortController();
|
|
79
|
+
const timeoutId = setTimeout(() => timeoutController.abort(), timeoutMs);
|
|
80
|
+
|
|
81
|
+
// Compose caller's signal + timeout signal. AbortSignal.any() is available
|
|
82
|
+
// in modern Node/Bun/browsers; fall back to a manual relay otherwise.
|
|
83
|
+
let composed: AbortSignal;
|
|
84
|
+
if (opts?.signal) {
|
|
85
|
+
if (typeof AbortSignal !== 'undefined' && typeof (AbortSignal as unknown as { any?: unknown }).any === 'function') {
|
|
86
|
+
composed = (AbortSignal as unknown as { any: (signals: AbortSignal[]) => AbortSignal }).any([
|
|
87
|
+
opts.signal,
|
|
88
|
+
timeoutController.signal,
|
|
89
|
+
]);
|
|
90
|
+
} else {
|
|
91
|
+
const relayController = new AbortController();
|
|
92
|
+
const relay = () => relayController.abort();
|
|
93
|
+
opts.signal.addEventListener('abort', relay, { once: true });
|
|
94
|
+
timeoutController.signal.addEventListener('abort', relay, { once: true });
|
|
95
|
+
composed = relayController.signal;
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
composed = timeoutController.signal;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
signal: composed,
|
|
103
|
+
timeoutSignal: timeoutController.signal,
|
|
104
|
+
timeoutMs,
|
|
105
|
+
cleanup: () => clearTimeout(timeoutId),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function readBodySafe(res: Response): Promise<unknown> {
|
|
110
|
+
const ct = res.headers.get('content-type') ?? '';
|
|
111
|
+
try {
|
|
112
|
+
if (ct.includes('application/json')) return await res.json();
|
|
113
|
+
return await res.text();
|
|
114
|
+
} catch {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function assertSolanaWallet(wallet: string): void {
|
|
120
|
+
if (!wallet || typeof wallet !== 'string') {
|
|
121
|
+
throw new AgentKarmaValidationError('wallet must be a non-empty string');
|
|
122
|
+
}
|
|
123
|
+
if (!SOLANA_ADDRESS_RE.test(wallet)) {
|
|
124
|
+
throw new AgentKarmaValidationError(
|
|
125
|
+
'wallet does not look like a Solana address (expected base58, 32-44 chars)',
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function assertAgentId(agentId: number): void {
|
|
131
|
+
if (!Number.isInteger(agentId) || agentId <= 0) {
|
|
132
|
+
throw new AgentKarmaValidationError('agentId must be a positive integer');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function assertChain(chain: unknown): asserts chain is Chain {
|
|
137
|
+
if (typeof chain !== 'string' || !SUPPORTED_CHAINS.includes(chain as Chain)) {
|
|
138
|
+
throw new AgentKarmaValidationError(
|
|
139
|
+
`chain must be one of ${SUPPORTED_CHAINS.join(', ')}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Validate a wallet for a chain-aware lookup. DELIBERATELY does NOT apply the
|
|
146
|
+
* Solana base58 shape to EVM/Stellar chains — bonds and successions span chains
|
|
147
|
+
* whose addresses are not base58 Solana keys. We never auto-detect the chain
|
|
148
|
+
* from the address (the chain is always passed explicitly). Solana keeps its
|
|
149
|
+
* strict shape; other chains only require a non-empty string and let the server
|
|
150
|
+
* be the authority (it keys by the composite (chain,address)).
|
|
151
|
+
*/
|
|
152
|
+
function assertWalletForChain(wallet: string, chain: Chain): void {
|
|
153
|
+
if (!wallet || typeof wallet !== 'string') {
|
|
154
|
+
throw new AgentKarmaValidationError('wallet must be a non-empty string');
|
|
155
|
+
}
|
|
156
|
+
if (chain === 'solana') {
|
|
157
|
+
assertSolanaWallet(wallet);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function request<T>(
|
|
162
|
+
cfg: InternalConfig,
|
|
163
|
+
path: string,
|
|
164
|
+
init: RequestInit,
|
|
165
|
+
opts: RequestOptions | undefined,
|
|
166
|
+
): Promise<T> {
|
|
167
|
+
const url = `${cfg.baseUrl}${path}`;
|
|
168
|
+
const { signal, timeoutSignal, timeoutMs, cleanup } = buildSignal(cfg.timeout, opts);
|
|
169
|
+
|
|
170
|
+
let res: Response;
|
|
171
|
+
try {
|
|
172
|
+
res = await cfg.fetchImpl(url, {
|
|
173
|
+
...init,
|
|
174
|
+
signal,
|
|
175
|
+
headers: { ...cfg.headers, ...(init.headers ?? {}), ...(opts?.headers ?? {}) },
|
|
176
|
+
});
|
|
177
|
+
} catch (err) {
|
|
178
|
+
cleanup();
|
|
179
|
+
// The timeout's own controller having fired is the most reliable signal
|
|
180
|
+
// that we exceeded the deadline — works regardless of which runtime's
|
|
181
|
+
// AbortError shape was thrown by fetch (Bun vs Node vs browser).
|
|
182
|
+
if (timeoutSignal.aborted) {
|
|
183
|
+
throw new AgentKarmaTimeoutError(timeoutMs);
|
|
184
|
+
}
|
|
185
|
+
if (err instanceof Error && (err.name === 'AbortError' || err.name === 'TimeoutError')) {
|
|
186
|
+
throw new AgentKarmaTimeoutError(timeoutMs);
|
|
187
|
+
}
|
|
188
|
+
throw new AgentKarmaNetworkError(
|
|
189
|
+
err instanceof Error ? err.message : String(err),
|
|
190
|
+
err,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
cleanup();
|
|
194
|
+
|
|
195
|
+
if (res.status === 429) {
|
|
196
|
+
const retryAfter = Number(res.headers.get('retry-after') ?? '0');
|
|
197
|
+
const body = await readBodySafe(res);
|
|
198
|
+
throw new AgentKarmaRateLimitError(
|
|
199
|
+
'AgentKarma rate limit exceeded',
|
|
200
|
+
{ response: body, retryAfter: Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : undefined },
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (res.status === 404) {
|
|
205
|
+
const body = await readBodySafe(res);
|
|
206
|
+
const msg = typeof body === 'object' && body && 'error' in body
|
|
207
|
+
? String((body as { error: unknown }).error)
|
|
208
|
+
: `Not found at ${path}`;
|
|
209
|
+
throw new AgentKarmaNotFoundError(msg, { response: body });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!res.ok) {
|
|
213
|
+
const body = await readBodySafe(res);
|
|
214
|
+
const msg = typeof body === 'object' && body && 'error' in body
|
|
215
|
+
? String((body as { error: unknown }).error)
|
|
216
|
+
: `AgentKarma server returned ${res.status}`;
|
|
217
|
+
throw new AgentKarmaServerError(msg, { status: res.status, response: body });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let parsed: unknown;
|
|
221
|
+
try {
|
|
222
|
+
parsed = await res.json();
|
|
223
|
+
} catch (err) {
|
|
224
|
+
throw new AgentKarmaMalformedResponseError(
|
|
225
|
+
'Response body is not valid JSON',
|
|
226
|
+
{ cause: err },
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
return parsed as T;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ─── Public client ────────────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
export interface AgentKarmaClient {
|
|
235
|
+
readonly baseUrl: string;
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* GET /api/v2/score/{wallet}?face={face} — full karma snapshot.
|
|
239
|
+
*
|
|
240
|
+
* `chain` is OPTIONAL and defaults to `'solana'` for back-compat. Pass it for
|
|
241
|
+
* celo/arc/stellar wallets so the SDK applies the right address validation
|
|
242
|
+
* (Solana base58 only applies to Solana). The server resolves the wallet's
|
|
243
|
+
* real chain from the address; we NEVER auto-detect an EVM chain client-side.
|
|
244
|
+
*/
|
|
245
|
+
getKarma(
|
|
246
|
+
wallet: string,
|
|
247
|
+
opts?: { face?: KarmaFace | 'both'; chain?: Chain } & RequestOptions,
|
|
248
|
+
): Promise<KarmaSnapshot>;
|
|
249
|
+
|
|
250
|
+
/** Shorthand for `getKarma(wallet, { face: 'provider' })`. Returns just the provider face. */
|
|
251
|
+
getProviderKarma(wallet: string, opts?: RequestOptions): Promise<KarmaFaceData>;
|
|
252
|
+
|
|
253
|
+
/** Shorthand for `getKarma(wallet, { face: 'consumer' })`. Returns just the consumer face. */
|
|
254
|
+
getConsumerKarma(wallet: string, opts?: RequestOptions): Promise<KarmaFaceData | null>;
|
|
255
|
+
|
|
256
|
+
/** GET /api/v2/celo/{agentId} — ERC-8004 IdentityRegistry + ReputationRegistry snapshot. */
|
|
257
|
+
getCeloAgent(agentId: number, opts?: RequestOptions): Promise<CeloAgentSnapshot>;
|
|
258
|
+
|
|
259
|
+
/** GET /api/search?q={query} — substring search over indexed wallets. */
|
|
260
|
+
searchAgents(query: string, opts?: { limit?: number } & RequestOptions): Promise<SearchResponse>;
|
|
261
|
+
|
|
262
|
+
/** GET /api/agent/{wallet}/history — paginated x402 history with feedback ratings. */
|
|
263
|
+
getAgentHistory(
|
|
264
|
+
wallet: string,
|
|
265
|
+
opts?: { limit?: number; offset?: number } & RequestOptions,
|
|
266
|
+
): Promise<AgentHistoryResponse>;
|
|
267
|
+
|
|
268
|
+
/** GET /api/feedback?agent={wallet} — aggregate delivered/failed counts. */
|
|
269
|
+
getFeedbackSummary(wallet: string, opts?: RequestOptions): Promise<FeedbackSummary>;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* GET /api/v2/succession/{chain}/{wallet} — declared Dead Man's Switch plan
|
|
273
|
+
* + AK's OBSERVED heartbeat liveness. `chain` is explicit (spans all chains);
|
|
274
|
+
* Solana wallets are shape-checked, others passed through. Throws
|
|
275
|
+
* AgentKarmaNotFoundError (404) when the agent declared no succession plan.
|
|
276
|
+
*/
|
|
277
|
+
getSuccessionStatus(
|
|
278
|
+
chain: Chain,
|
|
279
|
+
wallet: string,
|
|
280
|
+
opts?: RequestOptions,
|
|
281
|
+
): Promise<SuccessionView>;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* GET /api/v2/bond/{chain}/{wallet} → `.bonds` — bonds taken out ON this
|
|
285
|
+
* agent (open vs resolved, totalBondedUsdc, hasDemo). `chain` is explicit.
|
|
286
|
+
* Returns an empty block (no throw) when the agent has no bonds.
|
|
287
|
+
*/
|
|
288
|
+
getBondStatus(chain: Chain, wallet: string, opts?: RequestOptions): Promise<BondBlock>;
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* GET /api/v2/bond/{chain}/{wallet} → `.surety` — this wallet's ORTHOGONAL
|
|
292
|
+
* Surety Karma from underwriting OTHER agents' bonds. `chain` is explicit.
|
|
293
|
+
* Returns null when the wallet has never underwritten.
|
|
294
|
+
*/
|
|
295
|
+
getSuretyKarma(
|
|
296
|
+
chain: Chain,
|
|
297
|
+
wallet: string,
|
|
298
|
+
opts?: RequestOptions,
|
|
299
|
+
): Promise<SuretyView | null>;
|
|
300
|
+
|
|
301
|
+
/** POST /api/feedback — submit a wallet-signed consumer rating (Solana-only today). */
|
|
302
|
+
submitFeedback(input: FeedbackSubmission, opts?: RequestOptions): Promise<FeedbackSubmissionResponse>;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function createAgentKarmaClient(config?: ClientConfig): AgentKarmaClient {
|
|
306
|
+
const cfg = normalizeConfig(config);
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
baseUrl: cfg.baseUrl,
|
|
310
|
+
|
|
311
|
+
async getKarma(wallet, opts = {}) {
|
|
312
|
+
// Chain-aware: default 'solana' (back-compat). assertWalletForChain applies
|
|
313
|
+
// the strict base58 shape only for Solana; other chains pass through to the
|
|
314
|
+
// server. We do NOT auto-detect an EVM chain from the address.
|
|
315
|
+
const chain = opts.chain ?? 'solana';
|
|
316
|
+
assertChain(chain);
|
|
317
|
+
assertWalletForChain(wallet, chain);
|
|
318
|
+
const face = opts.face ?? 'both';
|
|
319
|
+
if (face !== 'both' && face !== 'provider' && face !== 'consumer') {
|
|
320
|
+
throw new AgentKarmaValidationError(`face must be 'provider' | 'consumer' | 'both'`);
|
|
321
|
+
}
|
|
322
|
+
const path = `/api/v2/score/${encodeURIComponent(wallet)}?face=${face}`;
|
|
323
|
+
const snap = await request<KarmaSnapshot>(cfg, path, { method: 'GET' }, opts);
|
|
324
|
+
validateKarmaSnapshot(snap);
|
|
325
|
+
return snap;
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
async getProviderKarma(wallet, opts) {
|
|
329
|
+
const snap = await this.getKarma(wallet, { ...opts, face: 'provider' });
|
|
330
|
+
if (!snap.provider) {
|
|
331
|
+
throw new AgentKarmaMalformedResponseError(
|
|
332
|
+
'Server omitted provider face from response',
|
|
333
|
+
{ response: snap },
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
return snap.provider;
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
async getConsumerKarma(wallet, opts) {
|
|
340
|
+
const snap = await this.getKarma(wallet, { ...opts, face: 'consumer' });
|
|
341
|
+
// Consumer face is allowed to be absent (the wallet may have no consumer
|
|
342
|
+
// signal). Surface null rather than throw — caller decides.
|
|
343
|
+
return snap.consumer ?? null;
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
async getCeloAgent(agentId, opts) {
|
|
347
|
+
assertAgentId(agentId);
|
|
348
|
+
const path = `/api/v2/celo/${agentId}`;
|
|
349
|
+
const snap = await request<CeloAgentSnapshot>(cfg, path, { method: 'GET' }, opts);
|
|
350
|
+
if (snap.chain !== 'celo' || typeof snap.owner !== 'string') {
|
|
351
|
+
throw new AgentKarmaMalformedResponseError(
|
|
352
|
+
'Celo agent response missing required fields',
|
|
353
|
+
{ response: snap },
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
return snap;
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
async searchAgents(query, opts = {}) {
|
|
360
|
+
if (typeof query !== 'string' || query.trim().length < 3) {
|
|
361
|
+
throw new AgentKarmaValidationError('query must be a string of at least 3 characters');
|
|
362
|
+
}
|
|
363
|
+
const params = new URLSearchParams({ q: query.trim() });
|
|
364
|
+
if (opts.limit != null) params.set('limit', String(opts.limit));
|
|
365
|
+
const path = `/api/search?${params.toString()}`;
|
|
366
|
+
const res = await request<SearchResponse>(cfg, path, { method: 'GET' }, opts);
|
|
367
|
+
if (!res || !Array.isArray(res.results)) {
|
|
368
|
+
throw new AgentKarmaMalformedResponseError(
|
|
369
|
+
'Search response missing results array',
|
|
370
|
+
{ response: res },
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
return res;
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
async getAgentHistory(wallet, opts = {}) {
|
|
377
|
+
assertSolanaWallet(wallet);
|
|
378
|
+
const params = new URLSearchParams();
|
|
379
|
+
if (opts.limit != null) params.set('limit', String(opts.limit));
|
|
380
|
+
if (opts.offset != null) params.set('offset', String(opts.offset));
|
|
381
|
+
const qs = params.toString();
|
|
382
|
+
const path = `/api/agent/${encodeURIComponent(wallet)}/history${qs ? `?${qs}` : ''}`;
|
|
383
|
+
const res = await request<AgentHistoryResponse>(cfg, path, { method: 'GET' }, opts);
|
|
384
|
+
if (!res || !Array.isArray(res.transactions)) {
|
|
385
|
+
throw new AgentKarmaMalformedResponseError(
|
|
386
|
+
'History response missing transactions array',
|
|
387
|
+
{ response: res },
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
return res;
|
|
391
|
+
},
|
|
392
|
+
|
|
393
|
+
async getFeedbackSummary(wallet, opts) {
|
|
394
|
+
assertSolanaWallet(wallet);
|
|
395
|
+
const path = `/api/feedback?agent=${encodeURIComponent(wallet)}`;
|
|
396
|
+
const res = await request<FeedbackSummary>(cfg, path, { method: 'GET' }, opts);
|
|
397
|
+
if (
|
|
398
|
+
typeof res !== 'object' ||
|
|
399
|
+
res === null ||
|
|
400
|
+
typeof (res as FeedbackSummary).total !== 'number'
|
|
401
|
+
) {
|
|
402
|
+
throw new AgentKarmaMalformedResponseError(
|
|
403
|
+
'Feedback summary response shape unexpected',
|
|
404
|
+
{ response: res },
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
return res;
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
async getSuccessionStatus(chain, wallet, opts) {
|
|
411
|
+
assertChain(chain);
|
|
412
|
+
assertWalletForChain(wallet, chain);
|
|
413
|
+
const path = `/api/v2/succession/${chain}/${encodeURIComponent(wallet)}`;
|
|
414
|
+
const raw = await request<unknown>(cfg, path, { method: 'GET' }, opts);
|
|
415
|
+
const res = raw as Partial<SuccessionResponse> | null;
|
|
416
|
+
const s = res?.succession as Record<string, unknown> | undefined;
|
|
417
|
+
if (!s || typeof s !== 'object') {
|
|
418
|
+
throw new AgentKarmaMalformedResponseError(
|
|
419
|
+
'Succession response missing succession block',
|
|
420
|
+
{ response: raw },
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
if (typeof s.status !== 'string' || typeof s.intervalSeconds !== 'number') {
|
|
424
|
+
throw new AgentKarmaMalformedResponseError(
|
|
425
|
+
'Succession view missing required fields (status, intervalSeconds)',
|
|
426
|
+
{ response: raw },
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
return s as unknown as SuccessionView;
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
async getBondStatus(chain, wallet, opts) {
|
|
433
|
+
assertChain(chain);
|
|
434
|
+
assertWalletForChain(wallet, chain);
|
|
435
|
+
const path = `/api/v2/bond/${chain}/${encodeURIComponent(wallet)}`;
|
|
436
|
+
const raw = await request<unknown>(cfg, path, { method: 'GET' }, opts);
|
|
437
|
+
const res = raw as Partial<BondResponse> | null;
|
|
438
|
+
const block = res?.bonds as Record<string, unknown> | undefined;
|
|
439
|
+
if (
|
|
440
|
+
!block ||
|
|
441
|
+
typeof block !== 'object' ||
|
|
442
|
+
!Array.isArray(block.open) ||
|
|
443
|
+
!Array.isArray(block.resolved) ||
|
|
444
|
+
typeof block.totalBondedUsdc !== 'number'
|
|
445
|
+
) {
|
|
446
|
+
throw new AgentKarmaMalformedResponseError(
|
|
447
|
+
'Bond response missing bonds block',
|
|
448
|
+
{ response: raw },
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
return block as unknown as BondBlock;
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
async getSuretyKarma(chain, wallet, opts) {
|
|
455
|
+
assertChain(chain);
|
|
456
|
+
assertWalletForChain(wallet, chain);
|
|
457
|
+
const path = `/api/v2/bond/${chain}/${encodeURIComponent(wallet)}`;
|
|
458
|
+
const res = await request<BondResponse>(cfg, path, { method: 'GET' }, opts);
|
|
459
|
+
// Surety is allowed to be absent (the wallet may never have underwritten).
|
|
460
|
+
// Surface null rather than throw — caller decides.
|
|
461
|
+
return res?.surety ?? null;
|
|
462
|
+
},
|
|
463
|
+
|
|
464
|
+
async submitFeedback(input, opts) {
|
|
465
|
+
if (!input || typeof input !== 'object') {
|
|
466
|
+
throw new AgentKarmaValidationError('feedback submission must be an object');
|
|
467
|
+
}
|
|
468
|
+
if (!input.agentWallet || !input.txSignature || !input.signature || !input.message) {
|
|
469
|
+
throw new AgentKarmaValidationError(
|
|
470
|
+
'feedback submission requires agentWallet, txSignature, signature, message',
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
if (input.rating !== 'delivered' && input.rating !== 'failed') {
|
|
474
|
+
throw new AgentKarmaValidationError(`rating must be 'delivered' or 'failed'`);
|
|
475
|
+
}
|
|
476
|
+
const res = await request<FeedbackSubmissionResponse>(
|
|
477
|
+
cfg,
|
|
478
|
+
'/api/feedback',
|
|
479
|
+
{
|
|
480
|
+
method: 'POST',
|
|
481
|
+
body: JSON.stringify(input),
|
|
482
|
+
headers: { 'Content-Type': 'application/json' },
|
|
483
|
+
},
|
|
484
|
+
opts,
|
|
485
|
+
);
|
|
486
|
+
if (res?.success !== true) {
|
|
487
|
+
throw new AgentKarmaMalformedResponseError(
|
|
488
|
+
'Feedback submission response missing success flag',
|
|
489
|
+
{ response: res },
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
return res;
|
|
493
|
+
},
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Lightweight runtime validation. We only assert presence of the load-bearing
|
|
499
|
+
* fields — the rest stays opaque so the server can evolve safely.
|
|
500
|
+
*/
|
|
501
|
+
function validateKarmaSnapshot(snap: unknown): asserts snap is KarmaSnapshot {
|
|
502
|
+
if (!snap || typeof snap !== 'object') {
|
|
503
|
+
throw new AgentKarmaMalformedResponseError('Karma snapshot is not an object', { response: snap });
|
|
504
|
+
}
|
|
505
|
+
const s = snap as Record<string, unknown>;
|
|
506
|
+
if (typeof s.address !== 'string') {
|
|
507
|
+
throw new AgentKarmaMalformedResponseError('Karma snapshot missing address', { response: snap });
|
|
508
|
+
}
|
|
509
|
+
if (typeof s.face !== 'string') {
|
|
510
|
+
throw new AgentKarmaMalformedResponseError('Karma snapshot missing face', { response: snap });
|
|
511
|
+
}
|
|
512
|
+
if (typeof s.autonomy !== 'object' || s.autonomy === null) {
|
|
513
|
+
throw new AgentKarmaMalformedResponseError('Karma snapshot missing autonomy block', { response: snap });
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Re-export the top-level error class so consumers can write a single
|
|
518
|
+
// instanceof check without importing the errors module separately.
|
|
519
|
+
export { AgentKarmaError };
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured errors thrown by the AgentKarma SDK.
|
|
3
|
+
*
|
|
4
|
+
* All errors derive from `AgentKarmaError`, so partner code can write a single
|
|
5
|
+
* `catch (err: unknown) { if (err instanceof AgentKarmaError) … }` block and
|
|
6
|
+
* narrow on subclass when finer handling is needed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Base class for every error thrown by the SDK. */
|
|
10
|
+
export class AgentKarmaError extends Error {
|
|
11
|
+
/** HTTP status code if the error originated from a response. */
|
|
12
|
+
public readonly status?: number;
|
|
13
|
+
/** Raw response body when available — useful for debugging unfamiliar errors. */
|
|
14
|
+
public readonly response?: unknown;
|
|
15
|
+
/** Underlying cause (network error, parse error, etc.). */
|
|
16
|
+
public override readonly cause?: unknown;
|
|
17
|
+
|
|
18
|
+
constructor(message: string, opts: { status?: number; response?: unknown; cause?: unknown } = {}) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'AgentKarmaError';
|
|
21
|
+
this.status = opts.status;
|
|
22
|
+
this.response = opts.response;
|
|
23
|
+
this.cause = opts.cause;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Caller passed an argument that failed local validation before the request was sent. */
|
|
28
|
+
export class AgentKarmaValidationError extends AgentKarmaError {
|
|
29
|
+
constructor(message: string) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = 'AgentKarmaValidationError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Server returned HTTP 404 — the requested wallet, agent, or resource doesn't exist. */
|
|
36
|
+
export class AgentKarmaNotFoundError extends AgentKarmaError {
|
|
37
|
+
constructor(message: string, opts: { response?: unknown } = {}) {
|
|
38
|
+
super(message, { status: 404, response: opts.response });
|
|
39
|
+
this.name = 'AgentKarmaNotFoundError';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Server returned HTTP 429 — caller is being rate-limited. */
|
|
44
|
+
export class AgentKarmaRateLimitError extends AgentKarmaError {
|
|
45
|
+
/** Seconds until the limit resets, when the server provides Retry-After. */
|
|
46
|
+
public readonly retryAfter?: number;
|
|
47
|
+
|
|
48
|
+
constructor(message: string, opts: { response?: unknown; retryAfter?: number } = {}) {
|
|
49
|
+
super(message, { status: 429, response: opts.response });
|
|
50
|
+
this.name = 'AgentKarmaRateLimitError';
|
|
51
|
+
this.retryAfter = opts.retryAfter;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Request did not complete within the configured timeout. */
|
|
56
|
+
export class AgentKarmaTimeoutError extends AgentKarmaError {
|
|
57
|
+
public readonly timeoutMs: number;
|
|
58
|
+
constructor(timeoutMs: number) {
|
|
59
|
+
super(`Request timed out after ${timeoutMs}ms`);
|
|
60
|
+
this.name = 'AgentKarmaTimeoutError';
|
|
61
|
+
this.timeoutMs = timeoutMs;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** fetch() threw before getting a response — DNS failure, connection refused, offline, etc. */
|
|
66
|
+
export class AgentKarmaNetworkError extends AgentKarmaError {
|
|
67
|
+
constructor(message: string, cause?: unknown) {
|
|
68
|
+
super(message, { cause });
|
|
69
|
+
this.name = 'AgentKarmaNetworkError';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Server returned HTTP 2xx but the body didn't match the expected shape. */
|
|
74
|
+
export class AgentKarmaMalformedResponseError extends AgentKarmaError {
|
|
75
|
+
constructor(message: string, opts: { response?: unknown; cause?: unknown } = {}) {
|
|
76
|
+
super(message, { response: opts.response, cause: opts.cause });
|
|
77
|
+
this.name = 'AgentKarmaMalformedResponseError';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Server returned a non-2xx status that doesn't fit a more specific category. */
|
|
82
|
+
export class AgentKarmaServerError extends AgentKarmaError {
|
|
83
|
+
constructor(message: string, opts: { status: number; response?: unknown }) {
|
|
84
|
+
super(message, { status: opts.status, response: opts.response });
|
|
85
|
+
this.name = 'AgentKarmaServerError';
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/feedback.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wallet-agnostic helpers for building consumer feedback messages.
|
|
3
|
+
*
|
|
4
|
+
* The server expects an Ed25519 signature (Solana-flavored) over a specific
|
|
5
|
+
* message string. This module produces the message; the SDK never asks for a
|
|
6
|
+
* private key. The caller signs externally and passes the signature to
|
|
7
|
+
* `client.submitFeedback()`.
|
|
8
|
+
*
|
|
9
|
+
* The message format is fixed by the server (see
|
|
10
|
+
* `web/src/app/api/feedback/route.ts`):
|
|
11
|
+
*
|
|
12
|
+
* AgentKarma: Feedback {rating} for {txSignature} at {timestamp}
|
|
13
|
+
*
|
|
14
|
+
* `{timestamp}` is Date.now() in milliseconds. The server enforces a 5-minute
|
|
15
|
+
* freshness window — don't pre-build messages and use them later.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { AgentKarmaValidationError } from './errors.js';
|
|
19
|
+
import type { FeedbackRating } from './types.js';
|
|
20
|
+
|
|
21
|
+
export interface BuildFeedbackMessageInput {
|
|
22
|
+
rating: FeedbackRating;
|
|
23
|
+
txSignature: string;
|
|
24
|
+
/** Defaults to Date.now() at call time. Server allows ±5 minutes. */
|
|
25
|
+
timestamp?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface BuiltFeedbackMessage {
|
|
29
|
+
/** The exact string to sign with the consumer wallet. */
|
|
30
|
+
message: string;
|
|
31
|
+
/** Timestamp embedded in the message — pass through to `submitFeedback()`. */
|
|
32
|
+
timestamp: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildFeedbackMessage(input: BuildFeedbackMessageInput): BuiltFeedbackMessage {
|
|
36
|
+
if (!input || typeof input !== 'object') {
|
|
37
|
+
throw new AgentKarmaValidationError('input is required');
|
|
38
|
+
}
|
|
39
|
+
if (input.rating !== 'delivered' && input.rating !== 'failed') {
|
|
40
|
+
throw new AgentKarmaValidationError(`rating must be 'delivered' or 'failed'`);
|
|
41
|
+
}
|
|
42
|
+
if (!input.txSignature || typeof input.txSignature !== 'string') {
|
|
43
|
+
throw new AgentKarmaValidationError('txSignature must be a non-empty string');
|
|
44
|
+
}
|
|
45
|
+
const timestamp = input.timestamp ?? Date.now();
|
|
46
|
+
if (!Number.isInteger(timestamp) || timestamp <= 0) {
|
|
47
|
+
throw new AgentKarmaValidationError('timestamp must be a positive integer (ms)');
|
|
48
|
+
}
|
|
49
|
+
const message = `AgentKarma: Feedback ${input.rating} for ${input.txSignature} at ${timestamp}`;
|
|
50
|
+
return { message, timestamp };
|
|
51
|
+
}
|