@elisym/sdk 0.11.0 → 0.12.1

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/dist/index.cjs CHANGED
@@ -62,8 +62,6 @@ function jobResultKind(offset) {
62
62
  var KIND_PING = 20200;
63
63
  var KIND_PONG = 20201;
64
64
  var LAMPORTS_PER_SOL = 1e9;
65
- var PROTOCOL_FEE_BPS = 300;
66
- var PROTOCOL_TREASURY = "GY7vnWMkKpftU4nQ16C2ATkj1JwrQpHhknkaBUn67VTy";
67
65
  var PROTOCOL_PROGRAM_ID_DEVNET = "BrX1CRkSgvcjxBvc2bgc3QqgWjinusofDmeP7ZVxvwrE";
68
66
  var ELISYM_PROTOCOL_TAG = "ELiZksgwDt41LaeuPDLkUfWgFXhGgVayTMP7L5nTSEL8";
69
67
  function getProtocolProgramId(cluster) {
@@ -1032,6 +1030,7 @@ async function createPaymentRequestWithOnchainConfig(rpc, programId, recipient,
1032
1030
  var RANKING_ACTIVITY_WINDOW_SECS = 30 * 24 * 60 * 60;
1033
1031
  var RANKING_BUCKET_SIZE_SECS = 60;
1034
1032
  var COLD_START_BUCKET = -Infinity;
1033
+ var NEVER_ABORTED_SIGNAL = new AbortController().signal;
1035
1034
  function toDTag(name) {
1036
1035
  const tag = name.toLowerCase().replace(/[^a-z0-9\s-]/g, (ch) => "_" + ch.charCodeAt(0).toString(16).padStart(2, "0")).replace(/\s+/g, "-").replace(/^-+|-+$/g, "");
1037
1036
  if (!tag) {
@@ -1061,13 +1060,63 @@ function compareAgentsByRank(a, b) {
1061
1060
  }
1062
1061
  return kb.lastSeen - ka.lastSeen;
1063
1062
  }
1063
+ function parseCapabilityEvent(event, network) {
1064
+ if (!nostrTools.verifyEvent(event)) {
1065
+ return null;
1066
+ }
1067
+ if (!event.content) {
1068
+ return null;
1069
+ }
1070
+ let raw;
1071
+ try {
1072
+ raw = JSON.parse(event.content);
1073
+ } catch {
1074
+ return null;
1075
+ }
1076
+ if (!raw || typeof raw !== "object") {
1077
+ return null;
1078
+ }
1079
+ const candidate = raw;
1080
+ if (typeof candidate.name !== "string" || !candidate.name) {
1081
+ return null;
1082
+ }
1083
+ if (typeof candidate.description !== "string") {
1084
+ return null;
1085
+ }
1086
+ if (!Array.isArray(candidate.capabilities) || !candidate.capabilities.every((cap) => typeof cap === "string")) {
1087
+ return null;
1088
+ }
1089
+ if (candidate.deleted) {
1090
+ return null;
1091
+ }
1092
+ const card = candidate;
1093
+ if (card.payment && (typeof card.payment.chain !== "string" || typeof card.payment.network !== "string" || typeof card.payment.address !== "string")) {
1094
+ return null;
1095
+ }
1096
+ if (card.payment?.job_price !== null && card.payment?.job_price !== void 0 && (!Number.isInteger(card.payment.job_price) || card.payment.job_price < 0)) {
1097
+ return null;
1098
+ }
1099
+ const agentNetwork = card.payment?.network ?? "devnet";
1100
+ if (agentNetwork !== network) {
1101
+ return null;
1102
+ }
1103
+ const kTags = event.tags.filter((tag) => tag[0] === "k").map((tag) => parseInt(tag[1] ?? "", 10)).filter((kind) => !isNaN(kind));
1104
+ return {
1105
+ pubkey: event.pubkey,
1106
+ npub: nostrTools.nip19.npubEncode(event.pubkey),
1107
+ cards: [card],
1108
+ eventId: event.id,
1109
+ supportedKinds: kTags,
1110
+ lastSeen: event.created_at
1111
+ };
1112
+ }
1064
1113
  function buildAgentsFromEvents(events, network) {
1065
1114
  const latestByDTag = /* @__PURE__ */ new Map();
1066
1115
  for (const event of events) {
1067
1116
  if (!nostrTools.verifyEvent(event)) {
1068
1117
  continue;
1069
1118
  }
1070
- const dTag = event.tags.find((t) => t[0] === "d")?.[1] ?? "";
1119
+ const dTag = event.tags.find((tag) => tag[0] === "d")?.[1] ?? "";
1071
1120
  const key = `${event.pubkey}:${dTag}`;
1072
1121
  const prev = latestByDTag.get(key);
1073
1122
  if (!prev || event.created_at > prev.created_at) {
@@ -1076,82 +1125,51 @@ function buildAgentsFromEvents(events, network) {
1076
1125
  }
1077
1126
  const accumMap = /* @__PURE__ */ new Map();
1078
1127
  for (const event of latestByDTag.values()) {
1079
- try {
1080
- if (!event.content) {
1081
- continue;
1082
- }
1083
- const raw = JSON.parse(event.content);
1084
- if (!raw || typeof raw !== "object") {
1085
- continue;
1086
- }
1087
- if (typeof raw.name !== "string" || !raw.name) {
1088
- continue;
1089
- }
1090
- if (typeof raw.description !== "string") {
1091
- continue;
1092
- }
1093
- if (!Array.isArray(raw.capabilities) || !raw.capabilities.every((c) => typeof c === "string")) {
1094
- continue;
1095
- }
1096
- if (raw.deleted) {
1097
- continue;
1098
- }
1099
- const card = raw;
1100
- if (card.payment && (typeof card.payment.chain !== "string" || typeof card.payment.network !== "string" || typeof card.payment.address !== "string")) {
1101
- continue;
1102
- }
1103
- if (card.payment?.job_price !== null && card.payment?.job_price !== void 0 && (!Number.isInteger(card.payment.job_price) || card.payment.job_price < 0)) {
1104
- continue;
1105
- }
1106
- const agentNetwork = card.payment?.network ?? "devnet";
1107
- if (agentNetwork !== network) {
1108
- continue;
1109
- }
1110
- const kTags = event.tags.filter((t) => t[0] === "k").map((t) => parseInt(t[1] ?? "", 10)).filter((k) => !isNaN(k));
1111
- const entry = { card, kTags, createdAt: event.created_at };
1112
- const existing = accumMap.get(event.pubkey);
1113
- if (existing) {
1114
- const dupIndex = existing.entries.findIndex((e) => e.card.name === card.name);
1115
- if (dupIndex >= 0) {
1116
- if (entry.createdAt >= existing.entries[dupIndex].createdAt) {
1117
- existing.entries[dupIndex] = entry;
1128
+ const parsed = parseCapabilityEvent(event, network);
1129
+ if (!parsed) {
1130
+ continue;
1131
+ }
1132
+ const card = parsed.cards[0];
1133
+ const cardKinds = parsed.supportedKinds;
1134
+ const createdAt = parsed.lastSeen;
1135
+ const existing = accumMap.get(parsed.pubkey);
1136
+ if (existing) {
1137
+ const prevForName = existing.perCard.get(card.name);
1138
+ if (prevForName) {
1139
+ if (createdAt >= prevForName.createdAt) {
1140
+ const idx = existing.agent.cards.findIndex(
1141
+ (existingCard) => existingCard.name === card.name
1142
+ );
1143
+ if (idx >= 0) {
1144
+ existing.agent.cards[idx] = card;
1118
1145
  }
1119
- } else {
1120
- existing.entries.push(entry);
1121
- }
1122
- if (event.created_at > existing.lastSeen) {
1123
- existing.lastSeen = event.created_at;
1124
- existing.eventId = event.id;
1146
+ existing.perCard.set(card.name, { createdAt, kTags: cardKinds });
1125
1147
  }
1126
1148
  } else {
1127
- accumMap.set(event.pubkey, {
1128
- pubkey: event.pubkey,
1129
- npub: nostrTools.nip19.npubEncode(event.pubkey),
1130
- entries: [entry],
1131
- eventId: event.id,
1132
- lastSeen: event.created_at
1133
- });
1149
+ existing.agent.cards.push(card);
1150
+ existing.perCard.set(card.name, { createdAt, kTags: cardKinds });
1134
1151
  }
1135
- } catch {
1152
+ if (createdAt > existing.agent.lastSeen) {
1153
+ existing.agent.lastSeen = createdAt;
1154
+ existing.agent.eventId = parsed.eventId;
1155
+ }
1156
+ } else {
1157
+ accumMap.set(parsed.pubkey, {
1158
+ agent: parsed,
1159
+ perCard: /* @__PURE__ */ new Map([[card.name, { createdAt, kTags: cardKinds }]])
1160
+ });
1136
1161
  }
1137
1162
  }
1138
1163
  const agentMap = /* @__PURE__ */ new Map();
1139
1164
  for (const [pubkey, acc] of accumMap) {
1140
1165
  const kindsSet = /* @__PURE__ */ new Set();
1141
- for (const e of acc.entries) {
1142
- for (const k of e.kTags) {
1143
- kindsSet.add(k);
1144
- }
1145
- }
1146
- const supportedKinds = [...kindsSet];
1147
- agentMap.set(pubkey, {
1148
- pubkey: acc.pubkey,
1149
- npub: acc.npub,
1150
- cards: acc.entries.map((e) => e.card),
1151
- eventId: acc.eventId,
1152
- supportedKinds,
1153
- lastSeen: acc.lastSeen
1154
- });
1166
+ for (const { kTags } of acc.perCard.values()) {
1167
+ for (const kind of kTags) {
1168
+ kindsSet.add(kind);
1169
+ }
1170
+ }
1171
+ acc.agent.supportedKinds = [...kindsSet];
1172
+ agentMap.set(pubkey, acc.agent);
1155
1173
  }
1156
1174
  return agentMap;
1157
1175
  }
@@ -1266,6 +1284,19 @@ var DiscoveryService = class {
1266
1284
  const events = await this.pool.querySync(filter);
1267
1285
  const agentMap = buildAgentsFromEvents(events, network);
1268
1286
  const agents = Array.from(agentMap.values());
1287
+ return this.runEnrichment(agents, agentMap, NEVER_ABORTED_SIGNAL);
1288
+ }
1289
+ /**
1290
+ * Enrich an agent map with paid-job stats, feedback counters, and kind:0
1291
+ * metadata, then return them sorted by `compareAgentsByRank`. Mutates the
1292
+ * passed-in `Agent` objects in place.
1293
+ *
1294
+ * Shared between `fetchAgents` (one-shot) and `streamAgents` (post-EOSE
1295
+ * second pass). The `signal` short-circuits the post-query work; in-flight
1296
+ * pool queries are not cancellable today (they fall through to the standard
1297
+ * timeout) and the caller drops the resolved value.
1298
+ */
1299
+ async runEnrichment(agents, agentMap, signal) {
1269
1300
  const agentPubkeys = Array.from(agentMap.keys());
1270
1301
  if (agentPubkeys.length === 0) {
1271
1302
  return agents;
@@ -1273,9 +1304,9 @@ var DiscoveryService = class {
1273
1304
  const activitySince = Math.floor(Date.now() / 1e3) - RANKING_ACTIVITY_WINDOW_SECS;
1274
1305
  const resultKinds = /* @__PURE__ */ new Set();
1275
1306
  for (const agent of agentMap.values()) {
1276
- for (const k of agent.supportedKinds) {
1277
- if (k >= KIND_JOB_REQUEST_BASE && k < KIND_JOB_RESULT_BASE) {
1278
- resultKinds.add(KIND_JOB_RESULT_BASE + (k - KIND_JOB_REQUEST_BASE));
1307
+ for (const supportedKind of agent.supportedKinds) {
1308
+ if (supportedKind >= KIND_JOB_REQUEST_BASE && supportedKind < KIND_JOB_RESULT_BASE) {
1309
+ resultKinds.add(KIND_JOB_RESULT_BASE + (supportedKind - KIND_JOB_REQUEST_BASE));
1279
1310
  }
1280
1311
  }
1281
1312
  }
@@ -1295,6 +1326,9 @@ var DiscoveryService = class {
1295
1326
  ),
1296
1327
  this.enrichWithMetadata(agents)
1297
1328
  ]);
1329
+ if (signal.aborted) {
1330
+ return agents;
1331
+ }
1298
1332
  const deliveredJobsByProvider = /* @__PURE__ */ new Map();
1299
1333
  for (const ev of resultEvents) {
1300
1334
  if (!nostrTools.verifyEvent(ev)) {
@@ -1307,7 +1341,7 @@ var DiscoveryService = class {
1307
1341
  if (ev.created_at > agent.lastSeen) {
1308
1342
  agent.lastSeen = ev.created_at;
1309
1343
  }
1310
- const jobEventId = ev.tags.find((t) => t[0] === "e")?.[1];
1344
+ const jobEventId = ev.tags.find((tag) => tag[0] === "e")?.[1];
1311
1345
  if (jobEventId) {
1312
1346
  let delivered = deliveredJobsByProvider.get(ev.pubkey);
1313
1347
  if (!delivered) {
@@ -1321,7 +1355,7 @@ var DiscoveryService = class {
1321
1355
  if (!nostrTools.verifyEvent(ev)) {
1322
1356
  continue;
1323
1357
  }
1324
- const targetPubkey = ev.tags.find((t) => t[0] === "p")?.[1];
1358
+ const targetPubkey = ev.tags.find((tag) => tag[0] === "p")?.[1];
1325
1359
  if (!targetPubkey) {
1326
1360
  continue;
1327
1361
  }
@@ -1332,17 +1366,17 @@ var DiscoveryService = class {
1332
1366
  if (ev.created_at > agent.lastSeen) {
1333
1367
  agent.lastSeen = ev.created_at;
1334
1368
  }
1335
- const rating = ev.tags.find((t) => t[0] === "rating")?.[1];
1369
+ const rating = ev.tags.find((tag) => tag[0] === "rating")?.[1];
1336
1370
  if (rating === "1" || rating === "0") {
1337
1371
  agent.totalRatingCount = (agent.totalRatingCount ?? 0) + 1;
1338
1372
  if (rating === "1") {
1339
1373
  agent.positiveCount = (agent.positiveCount ?? 0) + 1;
1340
1374
  }
1341
1375
  }
1342
- const status = ev.tags.find((t) => t[0] === "status")?.[1];
1343
- const txTag = ev.tags.find((t) => t[0] === "tx");
1376
+ const status = ev.tags.find((tag) => tag[0] === "status")?.[1];
1377
+ const txTag = ev.tags.find((tag) => tag[0] === "tx");
1344
1378
  const txSignature = txTag?.[1];
1345
- const jobEventId = ev.tags.find((t) => t[0] === "e")?.[1];
1379
+ const jobEventId = ev.tags.find((tag) => tag[0] === "e")?.[1];
1346
1380
  const hasDeliveredResult = jobEventId !== void 0 && deliveredJobsByProvider.get(targetPubkey)?.has(jobEventId) === true;
1347
1381
  if (status === "payment-completed" && typeof txSignature === "string" && txSignature && hasDeliveredResult) {
1348
1382
  if (!agent.lastPaidJobAt || ev.created_at > agent.lastPaidJobAt) {
@@ -1354,6 +1388,135 @@ var DiscoveryService = class {
1354
1388
  agents.sort(compareAgentsByRank);
1355
1389
  return agents;
1356
1390
  }
1391
+ /**
1392
+ * Stream elisym agents progressively as relays deliver events.
1393
+ *
1394
+ * Two live subscriptions:
1395
+ * - kind:31990 (capability cards) - emits `onAgent(agent)` for every new or
1396
+ * updated `(pubkey, d-tag)`. The emitted Agent is the merged view across
1397
+ * all surviving cards for that author.
1398
+ * - kind:6100 (default-offset job results) tagged `t=elisym` since 30d ago -
1399
+ * emits `onPaidJob(pubkey, ts)` for each delivered result. Custom-kind
1400
+ * results (offset != 100) are not on this stream; they enter the final
1401
+ * ranking via the post-EOSE enrichment pass.
1402
+ *
1403
+ * After capabilities EOSE, an enrichment pass runs in parallel to the live
1404
+ * subscriptions and produces a ranked snapshot via `onComplete`. The snapshot
1405
+ * is a clone, so further live updates do not mutate it.
1406
+ *
1407
+ * `closer.close()` tears down both subscriptions and aborts an in-flight
1408
+ * enrichment. If `opts.signal` is provided, aborting it does the same.
1409
+ */
1410
+ streamAgents(network, opts) {
1411
+ const eventsByPubkey = /* @__PURE__ */ new Map();
1412
+ const agentByPubkey = /* @__PURE__ */ new Map();
1413
+ const eoseSeen = { caps: false, results: false };
1414
+ let enrichmentStarted = false;
1415
+ const enrichmentAbort = new AbortController();
1416
+ const onExternalAbort = () => enrichmentAbort.abort();
1417
+ if (opts.signal) {
1418
+ if (opts.signal.aborted) {
1419
+ enrichmentAbort.abort();
1420
+ } else {
1421
+ opts.signal.addEventListener("abort", onExternalAbort, { once: true });
1422
+ }
1423
+ }
1424
+ const checkEose = () => {
1425
+ if (eoseSeen.caps && eoseSeen.results) {
1426
+ opts.onEose?.();
1427
+ }
1428
+ };
1429
+ const startEnrichment = () => {
1430
+ if (enrichmentStarted) {
1431
+ return;
1432
+ }
1433
+ enrichmentStarted = true;
1434
+ const snapshotAgents = Array.from(agentByPubkey.values()).map((agent) => ({ ...agent }));
1435
+ const snapshotMap = new Map(snapshotAgents.map((agent) => [agent.pubkey, agent]));
1436
+ void this.runEnrichment(snapshotAgents, snapshotMap, enrichmentAbort.signal).then(
1437
+ (sorted) => {
1438
+ if (enrichmentAbort.signal.aborted) {
1439
+ return;
1440
+ }
1441
+ opts.onComplete?.(sorted);
1442
+ },
1443
+ () => {
1444
+ }
1445
+ );
1446
+ };
1447
+ const capSub = this.pool.subscribe(
1448
+ { kinds: [KIND_APP_HANDLER], "#t": ["elisym"] },
1449
+ (event) => {
1450
+ const dTag = event.tags.find((tag) => tag[0] === "d")?.[1] ?? "";
1451
+ let perDTag = eventsByPubkey.get(event.pubkey);
1452
+ const prev = perDTag?.get(dTag);
1453
+ if (prev && event.created_at <= prev.created_at) {
1454
+ return;
1455
+ }
1456
+ if (!nostrTools.verifyEvent(event)) {
1457
+ return;
1458
+ }
1459
+ if (!event.content) {
1460
+ return;
1461
+ }
1462
+ let payload;
1463
+ try {
1464
+ payload = JSON.parse(event.content);
1465
+ } catch {
1466
+ return;
1467
+ }
1468
+ const isTombstone = payload !== null && typeof payload === "object" && Boolean(payload.deleted);
1469
+ if (!isTombstone && !parseCapabilityEvent(event, network)) {
1470
+ return;
1471
+ }
1472
+ if (!perDTag) {
1473
+ perDTag = /* @__PURE__ */ new Map();
1474
+ eventsByPubkey.set(event.pubkey, perDTag);
1475
+ }
1476
+ perDTag.set(dTag, event);
1477
+ const merged = buildAgentsFromEvents(Array.from(perDTag.values()), network).get(
1478
+ event.pubkey
1479
+ );
1480
+ if (!merged) {
1481
+ agentByPubkey.delete(event.pubkey);
1482
+ return;
1483
+ }
1484
+ agentByPubkey.set(event.pubkey, merged);
1485
+ opts.onAgent(merged);
1486
+ },
1487
+ {
1488
+ oneose: () => {
1489
+ eoseSeen.caps = true;
1490
+ startEnrichment();
1491
+ checkEose();
1492
+ }
1493
+ }
1494
+ );
1495
+ const activitySince = Math.floor(Date.now() / 1e3) - RANKING_ACTIVITY_WINDOW_SECS;
1496
+ const resultsSub = this.pool.subscribe(
1497
+ { kinds: [KIND_JOB_RESULT], "#t": ["elisym"], since: activitySince },
1498
+ (event) => {
1499
+ if (!nostrTools.verifyEvent(event)) {
1500
+ return;
1501
+ }
1502
+ opts.onPaidJob?.(event.pubkey, event.created_at);
1503
+ },
1504
+ {
1505
+ oneose: () => {
1506
+ eoseSeen.results = true;
1507
+ checkEose();
1508
+ }
1509
+ }
1510
+ );
1511
+ return {
1512
+ close: (reason) => {
1513
+ capSub.close(reason);
1514
+ resultsSub.close(reason);
1515
+ enrichmentAbort.abort();
1516
+ opts.signal?.removeEventListener("abort", onExternalAbort);
1517
+ }
1518
+ };
1519
+ }
1357
1520
  /**
1358
1521
  * Publish a capability card (kind:31990) as a provider.
1359
1522
  * Solana address is validated for Base58 format only - full decode
@@ -2569,8 +2732,11 @@ var NostrPool = class {
2569
2732
  throw new Error(`Failed to publish to all ${this.relays.length} relays`);
2570
2733
  }
2571
2734
  }
2572
- subscribe(filter, onEvent) {
2573
- const rawSub = this.pool.subscribeMany(this.relays, filter, { onevent: onEvent });
2735
+ subscribe(filter, onEvent, opts) {
2736
+ const rawSub = this.pool.subscribeMany(this.relays, filter, {
2737
+ onevent: onEvent,
2738
+ oneose: opts?.oneose
2739
+ });
2574
2740
  const tracked = {
2575
2741
  close: (reason) => {
2576
2742
  this.activeSubscriptions.delete(tracked);
@@ -3223,9 +3389,7 @@ exports.MarketplaceService = MarketplaceService;
3223
3389
  exports.MediaService = MediaService;
3224
3390
  exports.NATIVE_SOL = NATIVE_SOL;
3225
3391
  exports.NostrPool = NostrPool;
3226
- exports.PROTOCOL_FEE_BPS = PROTOCOL_FEE_BPS;
3227
3392
  exports.PROTOCOL_PROGRAM_ID_DEVNET = PROTOCOL_PROGRAM_ID_DEVNET;
3228
- exports.PROTOCOL_TREASURY = PROTOCOL_TREASURY;
3229
3393
  exports.PaymentRequestSchema = PaymentRequestSchema;
3230
3394
  exports.PingService = PingService;
3231
3395
  exports.RELAYS = RELAYS;