@elisym/sdk 0.9.0 → 0.10.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/dist/index.d.cts CHANGED
@@ -62,7 +62,16 @@ interface Agent {
62
62
  cards: CapabilityCard[];
63
63
  eventId: string;
64
64
  supportedKinds: number[];
65
+ /** Newest network signal of any kind: capability publish, result event, or feedback event. */
65
66
  lastSeen: number;
67
+ /** Unix seconds of the agent's most recent on-chain-verified paid job. Undefined if none. */
68
+ lastPaidJobAt?: number;
69
+ /** Solana tx signature of the verified paid job referenced by `lastPaidJobAt`. */
70
+ lastPaidJobTx?: string;
71
+ /** Count of `rating=1` feedback events targeting this agent (last 30 days). */
72
+ positiveCount?: number;
73
+ /** Count of all rated feedback events targeting this agent (last 30 days). */
74
+ totalRatingCount?: number;
66
75
  picture?: string;
67
76
  name?: string;
68
77
  about?: string;
@@ -325,9 +334,25 @@ declare class NostrPool {
325
334
 
326
335
  /** Convert a capability name to its Nostr d-tag form (ASCII-only, lowercase, hyphen-separated). */
327
336
  declare function toDTag(name: string): string;
337
+ /** Sort key derived from an Agent. Higher bucket / rate / lastPaidJobAt = ranks higher. */
338
+ interface RankKey {
339
+ /** Floor-to-minute timestamp of the agent's last verified paid job. `-Infinity` for cold start. */
340
+ bucket: number;
341
+ /** Positive review rate in `[0, 1]`. 0 when the agent has no rated feedback. */
342
+ rate: number;
343
+ /** Raw `lastPaidJobAt` (Unix sec) for tiebreak inside a bucket. 0 for cold start. */
344
+ lastPaidJobAt: number;
345
+ /** Final tiebreak; orders cold-start agents by NIP-89 freshness. */
346
+ lastSeen: number;
347
+ }
348
+ declare function computeRankKey(agent: Agent): RankKey;
349
+ declare function compareAgentsByRank(a: Agent, b: Agent): number;
328
350
  declare class DiscoveryService {
329
351
  private pool;
330
- constructor(pool: NostrPool);
352
+ private defaultRpc?;
353
+ constructor(pool: NostrPool, defaultRpc?: Rpc<SolanaRpcApi> | undefined);
354
+ /** Configure the Solana RPC used for on-chain payment verification in `fetchAgents`. */
355
+ setRpc(rpc: Rpc<SolanaRpcApi> | undefined): void;
331
356
  /** Count elisym agents (kind:31990 with "elisym" tag). */
332
357
  fetchAllAgentCount(): Promise<number>;
333
358
  /**
@@ -345,8 +370,21 @@ declare class DiscoveryService {
345
370
  }>;
346
371
  /** Enrich agents with kind:0 metadata (name, picture, about). Mutates in place and returns the same array. */
347
372
  enrichWithMetadata(agents: Agent[]): Promise<Agent[]>;
348
- /** Fetch elisym agents filtered by network. */
349
- fetchAgents(network?: Network, limit?: number): Promise<Agent[]>;
373
+ /**
374
+ * Fetch elisym agents filtered by network, ranked by verified paid-job recency.
375
+ *
376
+ * Ranking algorithm:
377
+ * 1. Bucket each agent into 1-minute slots by `lastPaidJobAt` (last on-chain
378
+ * verified payment). Cold-start agents (no verified paid job) go into a
379
+ * sentinel bucket below all populated buckets.
380
+ * 2. Within a bucket, sort by positive review rate descending.
381
+ * 3. Tiebreak by raw `lastPaidJobAt`, then `lastSeen` (NIP-89 freshness).
382
+ *
383
+ * On-chain verification uses {@link verifyJobPaymentQuick} - one-shot, cached.
384
+ * If `rpc` is not configured, all agents fall through to cold-start and order
385
+ * is determined by `lastSeen` only.
386
+ */
387
+ fetchAgents(network?: Network, limit?: number, rpcOverride?: Rpc<SolanaRpcApi>): Promise<Agent[]>;
350
388
  /**
351
389
  * Publish a capability card (kind:31990) as a provider.
352
390
  * Solana address is validated for Base58 format only - full decode
@@ -490,6 +528,11 @@ interface ElisymClientFullConfig extends ElisymClientConfig {
490
528
  payment?: PaymentStrategy;
491
529
  /** Custom upload URL for file uploads (defaults to nostr.build). */
492
530
  uploadUrl?: string;
531
+ /**
532
+ * Solana RPC used by `discovery.fetchAgents` for on-chain payment verification.
533
+ * If omitted, ranking falls back to NIP-89 freshness only (no paid-job promotion).
534
+ */
535
+ rpc?: Rpc<SolanaRpcApi>;
493
536
  }
494
537
  declare class ElisymClient {
495
538
  readonly pool: NostrPool;
@@ -766,6 +809,27 @@ type ParseResult = {
766
809
  */
767
810
  declare function parsePaymentRequest(input: string, options?: ParseOptions): ParseResult;
768
811
 
812
+ /**
813
+ * Lightweight payment verifier used by discovery ranking.
814
+ *
815
+ * Unlike `SolanaPaymentStrategy.verifyPayment`, this is a single-shot check
816
+ * with no retries: discovery cannot afford the 30-second confirmation budget
817
+ * the customer-side verifier uses. If the RPC has not seen the transaction
818
+ * yet, we treat the agent as "no verified paid job" rather than blocking.
819
+ *
820
+ * Positive results are cached forever (Solana txs are immutable once
821
+ * confirmed). Negative results expire after `NEGATIVE_CACHE_TTL_MS` so a
822
+ * just-confirmed tx will be picked up on the next discovery refresh.
823
+ */
824
+ type QuickVerifyReason = 'not_found' | 'tx_failed' | 'recipient_mismatch' | 'rpc_error' | 'invalid_input';
825
+ interface QuickVerifyResult {
826
+ verified: boolean;
827
+ txSignature: string;
828
+ reason?: QuickVerifyReason;
829
+ }
830
+ declare function clearQuickVerifyCache(): void;
831
+ declare function verifyJobPaymentQuick(rpc: Rpc<SolanaRpcApi>, txSignature: string, expectedRecipient: Address): Promise<QuickVerifyResult>;
832
+
769
833
  /**
770
834
  * Snapshot of the on-chain elisym-config program state.
771
835
  *
@@ -952,7 +1016,7 @@ declare function getProtocolProgramId(cluster: ProtocolCluster): Address;
952
1016
  /** Default values for timeouts, retries, and batch sizes. */
953
1017
  declare const DEFAULTS: {
954
1018
  readonly SUBSCRIPTION_TIMEOUT_MS: 120000;
955
- readonly PING_TIMEOUT_MS: 15000;
1019
+ readonly PING_TIMEOUT_MS: 3000;
956
1020
  readonly PING_RETRIES: 2;
957
1021
  readonly PING_CACHE_TTL_MS: 30000;
958
1022
  readonly PAYMENT_EXPIRY_SECS: 600;
@@ -978,4 +1042,4 @@ declare const LIMITS: {
978
1042
  readonly MAX_CAPABILITY_LENGTH: 64;
979
1043
  };
980
1044
 
981
- export { type Agent, Asset, BoundedSet, type BuildTransactionOptions, type CapabilityCard, DEFAULTS, DEFAULT_KIND_OFFSET, DEFAULT_REDACT_PATHS, DiscoveryService, ElisymClient, type ElisymClientConfig, type ElisymClientFullConfig, ElisymIdentity, type EstimatePriorityFeeOptions, type EstimateSolFeeOptions, type GetProtocolConfigOptions, INPUT_REDACT_PATHS, type Job, type JobStatus, type JobSubscriptionOptions, type JobUpdateCallbacks, KIND_APP_HANDLER, KIND_JOB_FEEDBACK, KIND_JOB_REQUEST, KIND_JOB_REQUEST_BASE, KIND_JOB_RESULT, KIND_JOB_RESULT_BASE, KIND_PING, KIND_PONG, LAMPORTS_PER_SOL, LIMITS, MarketplaceService, MediaService, type Network, type NetworkStats, NostrPool, PROTOCOL_FEE_BPS, PROTOCOL_PROGRAM_ID_DEVNET, PROTOCOL_TREASURY, type ParseOptions, type ParseResult, type ParsedPaymentRequest, type PaymentAssetRef, type PaymentInfo, type PaymentRequestData, PaymentRequestSchema, type PaymentStrategy, type PaymentValidationCode, type PaymentValidationError, type PingResult, PingService, type ProtocolCluster, type ProtocolConfig, type ProtocolConfigInput, RELAYS, type RateLimitDecision, SECRET_REDACT_PATHS, type Signer, type SlidingWindowLimiter, type SlidingWindowLimiterOptions, type SolFeeEstimate, SolanaPaymentStrategy, type SubCloser, type SubmitJobOptions, type VerifyOptions, type VerifyResult, assertExpiry, assertLamports, buildPaymentInstructions, calculateProtocolFee, clearPriorityFeeCache, clearProtocolConfigCache, createPaymentRequestWithOnchainConfig, createSlidingWindowLimiter, estimatePriorityFeeMicroLamports, estimateSolFeeLamports, formatFeeBreakdown, formatSol, getProtocolConfig, getProtocolProgramId, jobRequestKind, jobResultKind, makeCensor, nip44Decrypt, nip44Encrypt, parsePaymentRequest, pickPercentileFee, timeAgo, toDTag, truncateKey, validateAgentName, validateExpiry };
1045
+ export { type Agent, Asset, BoundedSet, type BuildTransactionOptions, type CapabilityCard, DEFAULTS, DEFAULT_KIND_OFFSET, DEFAULT_REDACT_PATHS, DiscoveryService, ElisymClient, type ElisymClientConfig, type ElisymClientFullConfig, ElisymIdentity, type EstimatePriorityFeeOptions, type EstimateSolFeeOptions, type GetProtocolConfigOptions, INPUT_REDACT_PATHS, type Job, type JobStatus, type JobSubscriptionOptions, type JobUpdateCallbacks, KIND_APP_HANDLER, KIND_JOB_FEEDBACK, KIND_JOB_REQUEST, KIND_JOB_REQUEST_BASE, KIND_JOB_RESULT, KIND_JOB_RESULT_BASE, KIND_PING, KIND_PONG, LAMPORTS_PER_SOL, LIMITS, MarketplaceService, MediaService, type Network, type NetworkStats, NostrPool, PROTOCOL_FEE_BPS, PROTOCOL_PROGRAM_ID_DEVNET, PROTOCOL_TREASURY, type ParseOptions, type ParseResult, type ParsedPaymentRequest, type PaymentAssetRef, type PaymentInfo, type PaymentRequestData, PaymentRequestSchema, type PaymentStrategy, type PaymentValidationCode, type PaymentValidationError, type PingResult, PingService, type ProtocolCluster, type ProtocolConfig, type ProtocolConfigInput, type QuickVerifyReason, type QuickVerifyResult, RELAYS, type RankKey, type RateLimitDecision, SECRET_REDACT_PATHS, type Signer, type SlidingWindowLimiter, type SlidingWindowLimiterOptions, type SolFeeEstimate, SolanaPaymentStrategy, type SubCloser, type SubmitJobOptions, type VerifyOptions, type VerifyResult, assertExpiry, assertLamports, buildPaymentInstructions, calculateProtocolFee, clearPriorityFeeCache, clearProtocolConfigCache, clearQuickVerifyCache, compareAgentsByRank, computeRankKey, createPaymentRequestWithOnchainConfig, createSlidingWindowLimiter, estimatePriorityFeeMicroLamports, estimateSolFeeLamports, formatFeeBreakdown, formatSol, getProtocolConfig, getProtocolProgramId, jobRequestKind, jobResultKind, makeCensor, nip44Decrypt, nip44Encrypt, parsePaymentRequest, pickPercentileFee, timeAgo, toDTag, truncateKey, validateAgentName, validateExpiry, verifyJobPaymentQuick };
package/dist/index.d.ts CHANGED
@@ -62,7 +62,16 @@ interface Agent {
62
62
  cards: CapabilityCard[];
63
63
  eventId: string;
64
64
  supportedKinds: number[];
65
+ /** Newest network signal of any kind: capability publish, result event, or feedback event. */
65
66
  lastSeen: number;
67
+ /** Unix seconds of the agent's most recent on-chain-verified paid job. Undefined if none. */
68
+ lastPaidJobAt?: number;
69
+ /** Solana tx signature of the verified paid job referenced by `lastPaidJobAt`. */
70
+ lastPaidJobTx?: string;
71
+ /** Count of `rating=1` feedback events targeting this agent (last 30 days). */
72
+ positiveCount?: number;
73
+ /** Count of all rated feedback events targeting this agent (last 30 days). */
74
+ totalRatingCount?: number;
66
75
  picture?: string;
67
76
  name?: string;
68
77
  about?: string;
@@ -325,9 +334,25 @@ declare class NostrPool {
325
334
 
326
335
  /** Convert a capability name to its Nostr d-tag form (ASCII-only, lowercase, hyphen-separated). */
327
336
  declare function toDTag(name: string): string;
337
+ /** Sort key derived from an Agent. Higher bucket / rate / lastPaidJobAt = ranks higher. */
338
+ interface RankKey {
339
+ /** Floor-to-minute timestamp of the agent's last verified paid job. `-Infinity` for cold start. */
340
+ bucket: number;
341
+ /** Positive review rate in `[0, 1]`. 0 when the agent has no rated feedback. */
342
+ rate: number;
343
+ /** Raw `lastPaidJobAt` (Unix sec) for tiebreak inside a bucket. 0 for cold start. */
344
+ lastPaidJobAt: number;
345
+ /** Final tiebreak; orders cold-start agents by NIP-89 freshness. */
346
+ lastSeen: number;
347
+ }
348
+ declare function computeRankKey(agent: Agent): RankKey;
349
+ declare function compareAgentsByRank(a: Agent, b: Agent): number;
328
350
  declare class DiscoveryService {
329
351
  private pool;
330
- constructor(pool: NostrPool);
352
+ private defaultRpc?;
353
+ constructor(pool: NostrPool, defaultRpc?: Rpc<SolanaRpcApi> | undefined);
354
+ /** Configure the Solana RPC used for on-chain payment verification in `fetchAgents`. */
355
+ setRpc(rpc: Rpc<SolanaRpcApi> | undefined): void;
331
356
  /** Count elisym agents (kind:31990 with "elisym" tag). */
332
357
  fetchAllAgentCount(): Promise<number>;
333
358
  /**
@@ -345,8 +370,21 @@ declare class DiscoveryService {
345
370
  }>;
346
371
  /** Enrich agents with kind:0 metadata (name, picture, about). Mutates in place and returns the same array. */
347
372
  enrichWithMetadata(agents: Agent[]): Promise<Agent[]>;
348
- /** Fetch elisym agents filtered by network. */
349
- fetchAgents(network?: Network, limit?: number): Promise<Agent[]>;
373
+ /**
374
+ * Fetch elisym agents filtered by network, ranked by verified paid-job recency.
375
+ *
376
+ * Ranking algorithm:
377
+ * 1. Bucket each agent into 1-minute slots by `lastPaidJobAt` (last on-chain
378
+ * verified payment). Cold-start agents (no verified paid job) go into a
379
+ * sentinel bucket below all populated buckets.
380
+ * 2. Within a bucket, sort by positive review rate descending.
381
+ * 3. Tiebreak by raw `lastPaidJobAt`, then `lastSeen` (NIP-89 freshness).
382
+ *
383
+ * On-chain verification uses {@link verifyJobPaymentQuick} - one-shot, cached.
384
+ * If `rpc` is not configured, all agents fall through to cold-start and order
385
+ * is determined by `lastSeen` only.
386
+ */
387
+ fetchAgents(network?: Network, limit?: number, rpcOverride?: Rpc<SolanaRpcApi>): Promise<Agent[]>;
350
388
  /**
351
389
  * Publish a capability card (kind:31990) as a provider.
352
390
  * Solana address is validated for Base58 format only - full decode
@@ -490,6 +528,11 @@ interface ElisymClientFullConfig extends ElisymClientConfig {
490
528
  payment?: PaymentStrategy;
491
529
  /** Custom upload URL for file uploads (defaults to nostr.build). */
492
530
  uploadUrl?: string;
531
+ /**
532
+ * Solana RPC used by `discovery.fetchAgents` for on-chain payment verification.
533
+ * If omitted, ranking falls back to NIP-89 freshness only (no paid-job promotion).
534
+ */
535
+ rpc?: Rpc<SolanaRpcApi>;
493
536
  }
494
537
  declare class ElisymClient {
495
538
  readonly pool: NostrPool;
@@ -766,6 +809,27 @@ type ParseResult = {
766
809
  */
767
810
  declare function parsePaymentRequest(input: string, options?: ParseOptions): ParseResult;
768
811
 
812
+ /**
813
+ * Lightweight payment verifier used by discovery ranking.
814
+ *
815
+ * Unlike `SolanaPaymentStrategy.verifyPayment`, this is a single-shot check
816
+ * with no retries: discovery cannot afford the 30-second confirmation budget
817
+ * the customer-side verifier uses. If the RPC has not seen the transaction
818
+ * yet, we treat the agent as "no verified paid job" rather than blocking.
819
+ *
820
+ * Positive results are cached forever (Solana txs are immutable once
821
+ * confirmed). Negative results expire after `NEGATIVE_CACHE_TTL_MS` so a
822
+ * just-confirmed tx will be picked up on the next discovery refresh.
823
+ */
824
+ type QuickVerifyReason = 'not_found' | 'tx_failed' | 'recipient_mismatch' | 'rpc_error' | 'invalid_input';
825
+ interface QuickVerifyResult {
826
+ verified: boolean;
827
+ txSignature: string;
828
+ reason?: QuickVerifyReason;
829
+ }
830
+ declare function clearQuickVerifyCache(): void;
831
+ declare function verifyJobPaymentQuick(rpc: Rpc<SolanaRpcApi>, txSignature: string, expectedRecipient: Address): Promise<QuickVerifyResult>;
832
+
769
833
  /**
770
834
  * Snapshot of the on-chain elisym-config program state.
771
835
  *
@@ -952,7 +1016,7 @@ declare function getProtocolProgramId(cluster: ProtocolCluster): Address;
952
1016
  /** Default values for timeouts, retries, and batch sizes. */
953
1017
  declare const DEFAULTS: {
954
1018
  readonly SUBSCRIPTION_TIMEOUT_MS: 120000;
955
- readonly PING_TIMEOUT_MS: 15000;
1019
+ readonly PING_TIMEOUT_MS: 3000;
956
1020
  readonly PING_RETRIES: 2;
957
1021
  readonly PING_CACHE_TTL_MS: 30000;
958
1022
  readonly PAYMENT_EXPIRY_SECS: 600;
@@ -978,4 +1042,4 @@ declare const LIMITS: {
978
1042
  readonly MAX_CAPABILITY_LENGTH: 64;
979
1043
  };
980
1044
 
981
- export { type Agent, Asset, BoundedSet, type BuildTransactionOptions, type CapabilityCard, DEFAULTS, DEFAULT_KIND_OFFSET, DEFAULT_REDACT_PATHS, DiscoveryService, ElisymClient, type ElisymClientConfig, type ElisymClientFullConfig, ElisymIdentity, type EstimatePriorityFeeOptions, type EstimateSolFeeOptions, type GetProtocolConfigOptions, INPUT_REDACT_PATHS, type Job, type JobStatus, type JobSubscriptionOptions, type JobUpdateCallbacks, KIND_APP_HANDLER, KIND_JOB_FEEDBACK, KIND_JOB_REQUEST, KIND_JOB_REQUEST_BASE, KIND_JOB_RESULT, KIND_JOB_RESULT_BASE, KIND_PING, KIND_PONG, LAMPORTS_PER_SOL, LIMITS, MarketplaceService, MediaService, type Network, type NetworkStats, NostrPool, PROTOCOL_FEE_BPS, PROTOCOL_PROGRAM_ID_DEVNET, PROTOCOL_TREASURY, type ParseOptions, type ParseResult, type ParsedPaymentRequest, type PaymentAssetRef, type PaymentInfo, type PaymentRequestData, PaymentRequestSchema, type PaymentStrategy, type PaymentValidationCode, type PaymentValidationError, type PingResult, PingService, type ProtocolCluster, type ProtocolConfig, type ProtocolConfigInput, RELAYS, type RateLimitDecision, SECRET_REDACT_PATHS, type Signer, type SlidingWindowLimiter, type SlidingWindowLimiterOptions, type SolFeeEstimate, SolanaPaymentStrategy, type SubCloser, type SubmitJobOptions, type VerifyOptions, type VerifyResult, assertExpiry, assertLamports, buildPaymentInstructions, calculateProtocolFee, clearPriorityFeeCache, clearProtocolConfigCache, createPaymentRequestWithOnchainConfig, createSlidingWindowLimiter, estimatePriorityFeeMicroLamports, estimateSolFeeLamports, formatFeeBreakdown, formatSol, getProtocolConfig, getProtocolProgramId, jobRequestKind, jobResultKind, makeCensor, nip44Decrypt, nip44Encrypt, parsePaymentRequest, pickPercentileFee, timeAgo, toDTag, truncateKey, validateAgentName, validateExpiry };
1045
+ export { type Agent, Asset, BoundedSet, type BuildTransactionOptions, type CapabilityCard, DEFAULTS, DEFAULT_KIND_OFFSET, DEFAULT_REDACT_PATHS, DiscoveryService, ElisymClient, type ElisymClientConfig, type ElisymClientFullConfig, ElisymIdentity, type EstimatePriorityFeeOptions, type EstimateSolFeeOptions, type GetProtocolConfigOptions, INPUT_REDACT_PATHS, type Job, type JobStatus, type JobSubscriptionOptions, type JobUpdateCallbacks, KIND_APP_HANDLER, KIND_JOB_FEEDBACK, KIND_JOB_REQUEST, KIND_JOB_REQUEST_BASE, KIND_JOB_RESULT, KIND_JOB_RESULT_BASE, KIND_PING, KIND_PONG, LAMPORTS_PER_SOL, LIMITS, MarketplaceService, MediaService, type Network, type NetworkStats, NostrPool, PROTOCOL_FEE_BPS, PROTOCOL_PROGRAM_ID_DEVNET, PROTOCOL_TREASURY, type ParseOptions, type ParseResult, type ParsedPaymentRequest, type PaymentAssetRef, type PaymentInfo, type PaymentRequestData, PaymentRequestSchema, type PaymentStrategy, type PaymentValidationCode, type PaymentValidationError, type PingResult, PingService, type ProtocolCluster, type ProtocolConfig, type ProtocolConfigInput, type QuickVerifyReason, type QuickVerifyResult, RELAYS, type RankKey, type RateLimitDecision, SECRET_REDACT_PATHS, type Signer, type SlidingWindowLimiter, type SlidingWindowLimiterOptions, type SolFeeEstimate, SolanaPaymentStrategy, type SubCloser, type SubmitJobOptions, type VerifyOptions, type VerifyResult, assertExpiry, assertLamports, buildPaymentInstructions, calculateProtocolFee, clearPriorityFeeCache, clearProtocolConfigCache, clearQuickVerifyCache, compareAgentsByRank, computeRankKey, createPaymentRequestWithOnchainConfig, createSlidingWindowLimiter, estimatePriorityFeeMicroLamports, estimateSolFeeLamports, formatFeeBreakdown, formatSol, getProtocolConfig, getProtocolProgramId, jobRequestKind, jobResultKind, makeCensor, nip44Decrypt, nip44Encrypt, parsePaymentRequest, pickPercentileFee, timeAgo, toDTag, truncateKey, validateAgentName, validateExpiry, verifyJobPaymentQuick };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { getTransferSolInstruction } from '@solana-program/system';
2
2
  import { findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS, getCreateAssociatedTokenIdempotentInstruction, ASSOCIATED_TOKEN_PROGRAM_ADDRESS, getTransferCheckedInstruction } from '@solana-program/token';
3
- import { pipe, createTransactionMessage, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, setTransactionMessageComputeUnitLimit, setTransactionMessageComputeUnitPrice, appendTransactionMessageInstructions, signTransactionMessageWithSigners, address, AccountRole, getProgramDerivedAddress, assertAccountExists, isAddress, getAddressDecoder, fetchEncodedAccount, decodeAccount, getStructDecoder, fixDecoderSize, getBytesDecoder, getU8Decoder, getOptionDecoder, getU16Decoder, getBooleanDecoder, getI64Decoder } from '@solana/kit';
3
+ import { pipe, createTransactionMessage, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, setTransactionMessageComputeUnitLimit, setTransactionMessageComputeUnitPrice, appendTransactionMessageInstructions, signTransactionMessageWithSigners, address, AccountRole, isAddress, getProgramDerivedAddress, assertAccountExists, getAddressDecoder, fetchEncodedAccount, decodeAccount, getStructDecoder, fixDecoderSize, getBytesDecoder, getU8Decoder, getOptionDecoder, getU16Decoder, getBooleanDecoder, getI64Decoder } from '@solana/kit';
4
4
  import Decimal2 from 'decimal.js-light';
5
5
  import { z } from 'zod';
6
6
  import { verifyEvent, finalizeEvent, getPublicKey, nip19, generateSecretKey, SimplePool } from 'nostr-tools';
@@ -50,7 +50,7 @@ function getProtocolProgramId(cluster) {
50
50
  }
51
51
  var DEFAULTS = {
52
52
  SUBSCRIPTION_TIMEOUT_MS: 12e4,
53
- PING_TIMEOUT_MS: 15e3,
53
+ PING_TIMEOUT_MS: 3e3,
54
54
  PING_RETRIES: 2,
55
55
  PING_CACHE_TTL_MS: 3e4,
56
56
  PAYMENT_EXPIRY_SECS: 600,
@@ -994,6 +994,95 @@ async function createPaymentRequestWithOnchainConfig(rpc, programId, recipient,
994
994
  options
995
995
  );
996
996
  }
997
+ var NEGATIVE_CACHE_TTL_MS = 6e4;
998
+ var verifyCache = /* @__PURE__ */ new Map();
999
+ function clearQuickVerifyCache() {
1000
+ verifyCache.clear();
1001
+ }
1002
+ async function verifyJobPaymentQuick(rpc, txSignature, expectedRecipient) {
1003
+ if (!txSignature) {
1004
+ return { verified: false, txSignature: "", reason: "invalid_input" };
1005
+ }
1006
+ if (!expectedRecipient || !isAddress(expectedRecipient)) {
1007
+ return { verified: false, txSignature, reason: "invalid_input" };
1008
+ }
1009
+ const cacheKey2 = `${txSignature}:${expectedRecipient}`;
1010
+ const cached = verifyCache.get(cacheKey2);
1011
+ if (cached) {
1012
+ if (cached.result.verified) {
1013
+ return cached.result;
1014
+ }
1015
+ if (Date.now() - cached.cachedAt < NEGATIVE_CACHE_TTL_MS) {
1016
+ return cached.result;
1017
+ }
1018
+ }
1019
+ const result = await doVerifyOnce(rpc, txSignature, expectedRecipient);
1020
+ verifyCache.set(cacheKey2, { result, cachedAt: Date.now() });
1021
+ return result;
1022
+ }
1023
+ async function doVerifyOnce(rpc, txSignature, expectedRecipient) {
1024
+ const sigStr = txSignature;
1025
+ if (!rpc || typeof rpc.getTransaction !== "function") {
1026
+ return { verified: false, txSignature: sigStr, reason: "rpc_error" };
1027
+ }
1028
+ let tx;
1029
+ try {
1030
+ tx = await rpc.getTransaction(txSignature, {
1031
+ commitment: "confirmed",
1032
+ encoding: "json",
1033
+ maxSupportedTransactionVersion: 0
1034
+ }).send();
1035
+ } catch {
1036
+ return { verified: false, txSignature: sigStr, reason: "rpc_error" };
1037
+ }
1038
+ if (!tx) {
1039
+ return { verified: false, txSignature: sigStr, reason: "not_found" };
1040
+ }
1041
+ if (!tx.meta || tx.meta.err) {
1042
+ return { verified: false, txSignature: sigStr, reason: "tx_failed" };
1043
+ }
1044
+ const accountKeys = tx.transaction.message.accountKeys;
1045
+ const recipientStr = expectedRecipient;
1046
+ const recipientIdx = accountKeys.indexOf(recipientStr);
1047
+ if (recipientIdx !== -1) {
1048
+ const preBalances = tx.meta.preBalances;
1049
+ const postBalances = tx.meta.postBalances;
1050
+ if (preBalances && postBalances) {
1051
+ const pre = preBalances[recipientIdx];
1052
+ const post = postBalances[recipientIdx];
1053
+ if (pre !== void 0 && post !== void 0) {
1054
+ const delta = BigInt(post) - BigInt(pre);
1055
+ if (delta > 0n) {
1056
+ return { verified: true, txSignature: sigStr };
1057
+ }
1058
+ }
1059
+ }
1060
+ }
1061
+ const postTokenBalances = tx.meta.postTokenBalances;
1062
+ const preTokenBalances = tx.meta.preTokenBalances;
1063
+ if (postTokenBalances) {
1064
+ for (const post of postTokenBalances) {
1065
+ if (post.owner !== recipientStr) {
1066
+ continue;
1067
+ }
1068
+ const pre = preTokenBalances?.find(
1069
+ (entry) => entry.owner === recipientStr && entry.mint === post.mint
1070
+ );
1071
+ const preAmount = pre ? BigInt(pre.uiTokenAmount.amount) : 0n;
1072
+ const postAmount = BigInt(post.uiTokenAmount.amount);
1073
+ if (postAmount > preAmount) {
1074
+ return { verified: true, txSignature: sigStr };
1075
+ }
1076
+ }
1077
+ }
1078
+ return { verified: false, txSignature: sigStr, reason: "recipient_mismatch" };
1079
+ }
1080
+
1081
+ // src/services/discovery.ts
1082
+ var RANKING_ACTIVITY_WINDOW_SECS = 30 * 24 * 60 * 60;
1083
+ var RANKING_BUCKET_SIZE_SECS = 60;
1084
+ var COLD_START_BUCKET = -Infinity;
1085
+ var MAX_PAID_CANDIDATES_PER_AGENT = 5;
997
1086
  function toDTag(name) {
998
1087
  const tag = name.toLowerCase().replace(/[^a-z0-9\s-]/g, (ch) => "_" + ch.charCodeAt(0).toString(16).padStart(2, "0")).replace(/\s+/g, "-").replace(/^-+|-+$/g, "");
999
1088
  if (!tag) {
@@ -1001,6 +1090,28 @@ function toDTag(name) {
1001
1090
  }
1002
1091
  return tag;
1003
1092
  }
1093
+ function computeRankKey(agent) {
1094
+ const lastPaidJobAt = agent.lastPaidJobAt ?? 0;
1095
+ const total = agent.totalRatingCount ?? 0;
1096
+ const positive = agent.positiveCount ?? 0;
1097
+ const rate = total > 0 ? positive / total : 0;
1098
+ const bucket = lastPaidJobAt > 0 ? Math.floor(lastPaidJobAt / RANKING_BUCKET_SIZE_SECS) * RANKING_BUCKET_SIZE_SECS : COLD_START_BUCKET;
1099
+ return { bucket, rate, lastPaidJobAt, lastSeen: agent.lastSeen };
1100
+ }
1101
+ function compareAgentsByRank(a, b) {
1102
+ const ka = computeRankKey(a);
1103
+ const kb = computeRankKey(b);
1104
+ if (kb.bucket !== ka.bucket) {
1105
+ return kb.bucket - ka.bucket;
1106
+ }
1107
+ if (kb.rate !== ka.rate) {
1108
+ return kb.rate - ka.rate;
1109
+ }
1110
+ if (kb.lastPaidJobAt !== ka.lastPaidJobAt) {
1111
+ return kb.lastPaidJobAt - ka.lastPaidJobAt;
1112
+ }
1113
+ return kb.lastSeen - ka.lastSeen;
1114
+ }
1004
1115
  function buildAgentsFromEvents(events, network) {
1005
1116
  const latestByDTag = /* @__PURE__ */ new Map();
1006
1117
  for (const event of events) {
@@ -1095,9 +1206,37 @@ function buildAgentsFromEvents(events, network) {
1095
1206
  }
1096
1207
  return agentMap;
1097
1208
  }
1209
+ function pickSolanaAddress(agent) {
1210
+ for (const card of agent.cards) {
1211
+ const addr = card.payment?.address;
1212
+ if (card.payment?.chain === "solana" && typeof addr === "string" && addr.length > 0) {
1213
+ return addr;
1214
+ }
1215
+ }
1216
+ return null;
1217
+ }
1218
+ async function verifyNewestPaidCandidate(rpc, recipient, candidatesNewestFirst) {
1219
+ const settled = await Promise.allSettled(
1220
+ candidatesNewestFirst.map(
1221
+ (candidate) => verifyJobPaymentQuick(rpc, candidate.txSignature, recipient)
1222
+ )
1223
+ );
1224
+ for (let i = 0; i < settled.length; i++) {
1225
+ const entry = settled[i];
1226
+ if (entry?.status === "fulfilled" && entry.value.verified) {
1227
+ return candidatesNewestFirst[i] ?? null;
1228
+ }
1229
+ }
1230
+ return null;
1231
+ }
1098
1232
  var DiscoveryService = class {
1099
- constructor(pool) {
1233
+ constructor(pool, defaultRpc) {
1100
1234
  this.pool = pool;
1235
+ this.defaultRpc = defaultRpc;
1236
+ }
1237
+ /** Configure the Solana RPC used for on-chain payment verification in `fetchAgents`. */
1238
+ setRpc(rpc) {
1239
+ this.defaultRpc = rpc;
1101
1240
  }
1102
1241
  /** Count elisym agents (kind:31990 with "elisym" tag). */
1103
1242
  async fetchAllAgentCount() {
@@ -1185,8 +1324,21 @@ var DiscoveryService = class {
1185
1324
  }
1186
1325
  return agents;
1187
1326
  }
1188
- /** Fetch elisym agents filtered by network. */
1189
- async fetchAgents(network = "devnet", limit) {
1327
+ /**
1328
+ * Fetch elisym agents filtered by network, ranked by verified paid-job recency.
1329
+ *
1330
+ * Ranking algorithm:
1331
+ * 1. Bucket each agent into 1-minute slots by `lastPaidJobAt` (last on-chain
1332
+ * verified payment). Cold-start agents (no verified paid job) go into a
1333
+ * sentinel bucket below all populated buckets.
1334
+ * 2. Within a bucket, sort by positive review rate descending.
1335
+ * 3. Tiebreak by raw `lastPaidJobAt`, then `lastSeen` (NIP-89 freshness).
1336
+ *
1337
+ * On-chain verification uses {@link verifyJobPaymentQuick} - one-shot, cached.
1338
+ * If `rpc` is not configured, all agents fall through to cold-start and order
1339
+ * is determined by `lastSeen` only.
1340
+ */
1341
+ async fetchAgents(network = "devnet", limit, rpcOverride) {
1190
1342
  const filter = {
1191
1343
  kinds: [KIND_APP_HANDLER],
1192
1344
  "#t": ["elisym"]
@@ -1196,40 +1348,100 @@ var DiscoveryService = class {
1196
1348
  }
1197
1349
  const events = await this.pool.querySync(filter);
1198
1350
  const agentMap = buildAgentsFromEvents(events, network);
1199
- const agents = Array.from(agentMap.values()).sort((a, b) => b.lastSeen - a.lastSeen);
1351
+ const agents = Array.from(agentMap.values());
1200
1352
  const agentPubkeys = Array.from(agentMap.keys());
1201
- if (agentPubkeys.length > 0) {
1202
- const activitySince = Math.floor(Date.now() / 1e3) - 24 * 60 * 60;
1203
- const resultKinds = /* @__PURE__ */ new Set();
1204
- for (const agent of agentMap.values()) {
1205
- for (const k of agent.supportedKinds) {
1206
- if (k >= KIND_JOB_REQUEST_BASE && k < KIND_JOB_RESULT_BASE) {
1207
- resultKinds.add(KIND_JOB_RESULT_BASE + (k - KIND_JOB_REQUEST_BASE));
1208
- }
1353
+ if (agentPubkeys.length === 0) {
1354
+ return agents;
1355
+ }
1356
+ const activitySince = Math.floor(Date.now() / 1e3) - RANKING_ACTIVITY_WINDOW_SECS;
1357
+ const resultKinds = /* @__PURE__ */ new Set();
1358
+ for (const agent of agentMap.values()) {
1359
+ for (const k of agent.supportedKinds) {
1360
+ if (k >= KIND_JOB_REQUEST_BASE && k < KIND_JOB_RESULT_BASE) {
1361
+ resultKinds.add(KIND_JOB_RESULT_BASE + (k - KIND_JOB_REQUEST_BASE));
1209
1362
  }
1210
1363
  }
1211
- resultKinds.add(jobResultKind(DEFAULT_KIND_OFFSET));
1212
- const [activityEvents] = await Promise.all([
1213
- this.pool.queryBatched(
1214
- {
1215
- kinds: [...resultKinds, KIND_JOB_FEEDBACK],
1216
- since: activitySince
1217
- },
1218
- agentPubkeys
1219
- ),
1220
- this.enrichWithMetadata(agents)
1221
- ]);
1222
- for (const ev of activityEvents) {
1223
- if (!verifyEvent(ev)) {
1224
- continue;
1364
+ }
1365
+ resultKinds.add(jobResultKind(DEFAULT_KIND_OFFSET));
1366
+ const [resultEvents, feedbackEvents] = await Promise.all([
1367
+ this.pool.queryBatched(
1368
+ {
1369
+ kinds: [...resultKinds],
1370
+ since: activitySince
1371
+ },
1372
+ agentPubkeys
1373
+ ),
1374
+ this.pool.queryBatchedByTag(
1375
+ { kinds: [KIND_JOB_FEEDBACK], since: activitySince },
1376
+ "p",
1377
+ agentPubkeys
1378
+ ),
1379
+ this.enrichWithMetadata(agents)
1380
+ ]);
1381
+ for (const ev of resultEvents) {
1382
+ if (!verifyEvent(ev)) {
1383
+ continue;
1384
+ }
1385
+ const agent = agentMap.get(ev.pubkey);
1386
+ if (agent && ev.created_at > agent.lastSeen) {
1387
+ agent.lastSeen = ev.created_at;
1388
+ }
1389
+ }
1390
+ const paidCandidates = /* @__PURE__ */ new Map();
1391
+ for (const ev of feedbackEvents) {
1392
+ if (!verifyEvent(ev)) {
1393
+ continue;
1394
+ }
1395
+ const targetPubkey = ev.tags.find((t) => t[0] === "p")?.[1];
1396
+ if (!targetPubkey) {
1397
+ continue;
1398
+ }
1399
+ const agent = agentMap.get(targetPubkey);
1400
+ if (!agent) {
1401
+ continue;
1402
+ }
1403
+ if (ev.created_at > agent.lastSeen) {
1404
+ agent.lastSeen = ev.created_at;
1405
+ }
1406
+ const rating = ev.tags.find((t) => t[0] === "rating")?.[1];
1407
+ if (rating === "1" || rating === "0") {
1408
+ agent.totalRatingCount = (agent.totalRatingCount ?? 0) + 1;
1409
+ if (rating === "1") {
1410
+ agent.positiveCount = (agent.positiveCount ?? 0) + 1;
1225
1411
  }
1226
- const agent = agentMap.get(ev.pubkey);
1227
- if (agent && ev.created_at > agent.lastSeen) {
1228
- agent.lastSeen = ev.created_at;
1412
+ }
1413
+ const status = ev.tags.find((t) => t[0] === "status")?.[1];
1414
+ const txTag = ev.tags.find((t) => t[0] === "tx");
1415
+ const txSignature = txTag?.[1];
1416
+ if (status === "payment-completed" && typeof txSignature === "string" && txSignature) {
1417
+ const list = paidCandidates.get(targetPubkey);
1418
+ const candidate = { txSignature, createdAt: ev.created_at };
1419
+ if (list) {
1420
+ list.push(candidate);
1421
+ } else {
1422
+ paidCandidates.set(targetPubkey, [candidate]);
1229
1423
  }
1230
1424
  }
1231
- agents.sort((a, b) => b.lastSeen - a.lastSeen);
1232
1425
  }
1426
+ const rpc = rpcOverride ?? this.defaultRpc;
1427
+ if (rpc && paidCandidates.size > 0) {
1428
+ const perAgent = Array.from(paidCandidates.entries()).map(([agentPubkey, list]) => {
1429
+ const agent = agentMap.get(agentPubkey);
1430
+ const recipient = agent ? pickSolanaAddress(agent) : null;
1431
+ if (!agent || !recipient) {
1432
+ return Promise.resolve();
1433
+ }
1434
+ const ordered = [...list].sort((a, b) => b.createdAt - a.createdAt).slice(0, MAX_PAID_CANDIDATES_PER_AGENT);
1435
+ return verifyNewestPaidCandidate(rpc, recipient, ordered).then((winner) => {
1436
+ if (winner) {
1437
+ agent.lastPaidJobAt = winner.createdAt;
1438
+ agent.lastPaidJobTx = winner.txSignature;
1439
+ }
1440
+ });
1441
+ });
1442
+ await Promise.all(perAgent);
1443
+ }
1444
+ agents.sort(compareAgentsByRank);
1233
1445
  return agents;
1234
1446
  }
1235
1447
  /**
@@ -2577,7 +2789,7 @@ var ElisymClient = class {
2577
2789
  payment;
2578
2790
  constructor(config = {}) {
2579
2791
  this.pool = new NostrPool(config.relays ?? RELAYS);
2580
- this.discovery = new DiscoveryService(this.pool);
2792
+ this.discovery = new DiscoveryService(this.pool, config.rpc);
2581
2793
  this.marketplace = new MarketplaceService(this.pool);
2582
2794
  this.ping = new PingService(this.pool);
2583
2795
  this.media = new MediaService(config.uploadUrl);
@@ -2905,6 +3117,6 @@ function makeCensor() {
2905
3117
  };
2906
3118
  }
2907
3119
 
2908
- export { BoundedSet, DEFAULTS, DEFAULT_KIND_OFFSET, DEFAULT_REDACT_PATHS, DiscoveryService, ElisymClient, ElisymIdentity, GlobalConfigSchema, INPUT_REDACT_PATHS, KIND_APP_HANDLER, KIND_JOB_FEEDBACK, KIND_JOB_REQUEST, KIND_JOB_REQUEST_BASE, KIND_JOB_RESULT, KIND_JOB_RESULT_BASE, KIND_PING, KIND_PONG, KNOWN_ASSETS, LAMPORTS_PER_SOL, LIMITS, MarketplaceService, MediaService, NATIVE_SOL, NostrPool, PROTOCOL_FEE_BPS, PROTOCOL_PROGRAM_ID_DEVNET, PROTOCOL_TREASURY, PaymentRequestSchema, PingService, RELAYS, SECRET_REDACT_PATHS, SessionSpendLimitEntrySchema, SolanaPaymentStrategy, USDC_SOLANA_DEVNET, assertExpiry, assertLamports, assetByKey, assetKey, buildPaymentInstructions, calculateProtocolFee, clearPriorityFeeCache, clearProtocolConfigCache, createPaymentRequestWithOnchainConfig, createSlidingWindowLimiter, estimatePriorityFeeMicroLamports, estimateSolFeeLamports, formatAssetAmount, formatFeeBreakdown, formatSol, getProtocolConfig, getProtocolProgramId, jobRequestKind, jobResultKind, makeCensor, nip44Decrypt, nip44Encrypt, parseAssetAmount, parsePaymentRequest, pickPercentileFee, resolveAssetFromPaymentRequest, resolveKnownAsset, timeAgo, toDTag, truncateKey, validateAgentName, validateExpiry };
3120
+ export { BoundedSet, DEFAULTS, DEFAULT_KIND_OFFSET, DEFAULT_REDACT_PATHS, DiscoveryService, ElisymClient, ElisymIdentity, GlobalConfigSchema, INPUT_REDACT_PATHS, KIND_APP_HANDLER, KIND_JOB_FEEDBACK, KIND_JOB_REQUEST, KIND_JOB_REQUEST_BASE, KIND_JOB_RESULT, KIND_JOB_RESULT_BASE, KIND_PING, KIND_PONG, KNOWN_ASSETS, LAMPORTS_PER_SOL, LIMITS, MarketplaceService, MediaService, NATIVE_SOL, NostrPool, PROTOCOL_FEE_BPS, PROTOCOL_PROGRAM_ID_DEVNET, PROTOCOL_TREASURY, PaymentRequestSchema, PingService, RELAYS, SECRET_REDACT_PATHS, SessionSpendLimitEntrySchema, SolanaPaymentStrategy, USDC_SOLANA_DEVNET, assertExpiry, assertLamports, assetByKey, assetKey, buildPaymentInstructions, calculateProtocolFee, clearPriorityFeeCache, clearProtocolConfigCache, clearQuickVerifyCache, compareAgentsByRank, computeRankKey, createPaymentRequestWithOnchainConfig, createSlidingWindowLimiter, estimatePriorityFeeMicroLamports, estimateSolFeeLamports, formatAssetAmount, formatFeeBreakdown, formatSol, getProtocolConfig, getProtocolProgramId, jobRequestKind, jobResultKind, makeCensor, nip44Decrypt, nip44Encrypt, parseAssetAmount, parsePaymentRequest, pickPercentileFee, resolveAssetFromPaymentRequest, resolveKnownAsset, timeAgo, toDTag, truncateKey, validateAgentName, validateExpiry, verifyJobPaymentQuick };
2909
3121
  //# sourceMappingURL=index.js.map
2910
3122
  //# sourceMappingURL=index.js.map