@elisym/sdk 0.9.0 → 0.10.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.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;
@@ -88,6 +97,13 @@ interface Job {
88
97
  amount?: number;
89
98
  txHash?: string;
90
99
  createdAt: number;
100
+ /**
101
+ * Payment asset, derived from the `payment-required` feedback's embedded
102
+ * payment request when present. Undefined means either no payment-required
103
+ * feedback was observed for this job, or the embedded request was missing
104
+ * an `asset` field (treated as native SOL by callers).
105
+ */
106
+ asset?: PaymentAssetRef;
91
107
  }
92
108
  interface SubmitJobOptions {
93
109
  /** Job input text. Sent unencrypted if providerPubkey is not set. */
@@ -325,6 +341,19 @@ declare class NostrPool {
325
341
 
326
342
  /** Convert a capability name to its Nostr d-tag form (ASCII-only, lowercase, hyphen-separated). */
327
343
  declare function toDTag(name: string): string;
344
+ /** Sort key derived from an Agent. Higher bucket / rate / lastPaidJobAt = ranks higher. */
345
+ interface RankKey {
346
+ /** Floor-to-minute timestamp of the agent's last verified paid job. `-Infinity` for cold start. */
347
+ bucket: number;
348
+ /** Positive review rate in `[0, 1]`. 0 when the agent has no rated feedback. */
349
+ rate: number;
350
+ /** Raw `lastPaidJobAt` (Unix sec) for tiebreak inside a bucket. 0 for cold start. */
351
+ lastPaidJobAt: number;
352
+ /** Final tiebreak; orders cold-start agents by NIP-89 freshness. */
353
+ lastSeen: number;
354
+ }
355
+ declare function computeRankKey(agent: Agent): RankKey;
356
+ declare function compareAgentsByRank(a: Agent, b: Agent): number;
328
357
  declare class DiscoveryService {
329
358
  private pool;
330
359
  constructor(pool: NostrPool);
@@ -345,7 +374,26 @@ declare class DiscoveryService {
345
374
  }>;
346
375
  /** Enrich agents with kind:0 metadata (name, picture, about). Mutates in place and returns the same array. */
347
376
  enrichWithMetadata(agents: Agent[]): Promise<Agent[]>;
348
- /** Fetch elisym agents filtered by network. */
377
+ /**
378
+ * Fetch elisym agents filtered by network, ranked by paid-job recency and
379
+ * positive-feedback rate.
380
+ *
381
+ * Ranking algorithm:
382
+ * 1. Bucket each agent into 1-minute slots by `lastPaidJobAt` (newest
383
+ * `payment-completed` feedback timestamp). Cold-start agents go into a
384
+ * sentinel bucket below all populated buckets.
385
+ * 2. Within a bucket, sort by positive review rate descending.
386
+ * 3. Tiebreak by raw `lastPaidJobAt`, then `lastSeen` (NIP-89 freshness).
387
+ *
388
+ * NOTE: We trust the `payment-completed` feedback timestamp directly; we do
389
+ * not verify the embedded `tx` signature on-chain. Public Solana devnet RPC
390
+ * rate-limits trivially exceed what discovery needs (N agents * up-to-5
391
+ * candidates), and the resulting 429s blocked discovery entirely. This
392
+ * means a malicious customer can publish a fake `payment-completed` to lift
393
+ * an agent's ranking. Acceptable trade-off for devnet / MVP; tighten via
394
+ * recipient-tied checks when the network moves to mainnet with a paid RPC
395
+ * provider.
396
+ */
349
397
  fetchAgents(network?: Network, limit?: number): Promise<Agent[]>;
350
398
  /**
351
399
  * Publish a capability card (kind:31990) as a provider.
@@ -766,6 +814,27 @@ type ParseResult = {
766
814
  */
767
815
  declare function parsePaymentRequest(input: string, options?: ParseOptions): ParseResult;
768
816
 
817
+ /**
818
+ * Lightweight payment verifier used by discovery ranking.
819
+ *
820
+ * Unlike `SolanaPaymentStrategy.verifyPayment`, this is a single-shot check
821
+ * with no retries: discovery cannot afford the 30-second confirmation budget
822
+ * the customer-side verifier uses. If the RPC has not seen the transaction
823
+ * yet, we treat the agent as "no verified paid job" rather than blocking.
824
+ *
825
+ * Positive results are cached forever (Solana txs are immutable once
826
+ * confirmed). Negative results expire after `NEGATIVE_CACHE_TTL_MS` so a
827
+ * just-confirmed tx will be picked up on the next discovery refresh.
828
+ */
829
+ type QuickVerifyReason = 'not_found' | 'tx_failed' | 'recipient_mismatch' | 'rpc_error' | 'invalid_input';
830
+ interface QuickVerifyResult {
831
+ verified: boolean;
832
+ txSignature: string;
833
+ reason?: QuickVerifyReason;
834
+ }
835
+ declare function clearQuickVerifyCache(): void;
836
+ declare function verifyJobPaymentQuick(rpc: Rpc<SolanaRpcApi>, txSignature: string, expectedRecipient: Address): Promise<QuickVerifyResult>;
837
+
769
838
  /**
770
839
  * Snapshot of the on-chain elisym-config program state.
771
840
  *
@@ -952,7 +1021,7 @@ declare function getProtocolProgramId(cluster: ProtocolCluster): Address;
952
1021
  /** Default values for timeouts, retries, and batch sizes. */
953
1022
  declare const DEFAULTS: {
954
1023
  readonly SUBSCRIPTION_TIMEOUT_MS: 120000;
955
- readonly PING_TIMEOUT_MS: 15000;
1024
+ readonly PING_TIMEOUT_MS: 3000;
956
1025
  readonly PING_RETRIES: 2;
957
1026
  readonly PING_CACHE_TTL_MS: 30000;
958
1027
  readonly PAYMENT_EXPIRY_SECS: 600;
@@ -978,4 +1047,4 @@ declare const LIMITS: {
978
1047
  readonly MAX_CAPABILITY_LENGTH: 64;
979
1048
  };
980
1049
 
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 };
1050
+ 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;
@@ -88,6 +97,13 @@ interface Job {
88
97
  amount?: number;
89
98
  txHash?: string;
90
99
  createdAt: number;
100
+ /**
101
+ * Payment asset, derived from the `payment-required` feedback's embedded
102
+ * payment request when present. Undefined means either no payment-required
103
+ * feedback was observed for this job, or the embedded request was missing
104
+ * an `asset` field (treated as native SOL by callers).
105
+ */
106
+ asset?: PaymentAssetRef;
91
107
  }
92
108
  interface SubmitJobOptions {
93
109
  /** Job input text. Sent unencrypted if providerPubkey is not set. */
@@ -325,6 +341,19 @@ declare class NostrPool {
325
341
 
326
342
  /** Convert a capability name to its Nostr d-tag form (ASCII-only, lowercase, hyphen-separated). */
327
343
  declare function toDTag(name: string): string;
344
+ /** Sort key derived from an Agent. Higher bucket / rate / lastPaidJobAt = ranks higher. */
345
+ interface RankKey {
346
+ /** Floor-to-minute timestamp of the agent's last verified paid job. `-Infinity` for cold start. */
347
+ bucket: number;
348
+ /** Positive review rate in `[0, 1]`. 0 when the agent has no rated feedback. */
349
+ rate: number;
350
+ /** Raw `lastPaidJobAt` (Unix sec) for tiebreak inside a bucket. 0 for cold start. */
351
+ lastPaidJobAt: number;
352
+ /** Final tiebreak; orders cold-start agents by NIP-89 freshness. */
353
+ lastSeen: number;
354
+ }
355
+ declare function computeRankKey(agent: Agent): RankKey;
356
+ declare function compareAgentsByRank(a: Agent, b: Agent): number;
328
357
  declare class DiscoveryService {
329
358
  private pool;
330
359
  constructor(pool: NostrPool);
@@ -345,7 +374,26 @@ declare class DiscoveryService {
345
374
  }>;
346
375
  /** Enrich agents with kind:0 metadata (name, picture, about). Mutates in place and returns the same array. */
347
376
  enrichWithMetadata(agents: Agent[]): Promise<Agent[]>;
348
- /** Fetch elisym agents filtered by network. */
377
+ /**
378
+ * Fetch elisym agents filtered by network, ranked by paid-job recency and
379
+ * positive-feedback rate.
380
+ *
381
+ * Ranking algorithm:
382
+ * 1. Bucket each agent into 1-minute slots by `lastPaidJobAt` (newest
383
+ * `payment-completed` feedback timestamp). Cold-start agents go into a
384
+ * sentinel bucket below all populated buckets.
385
+ * 2. Within a bucket, sort by positive review rate descending.
386
+ * 3. Tiebreak by raw `lastPaidJobAt`, then `lastSeen` (NIP-89 freshness).
387
+ *
388
+ * NOTE: We trust the `payment-completed` feedback timestamp directly; we do
389
+ * not verify the embedded `tx` signature on-chain. Public Solana devnet RPC
390
+ * rate-limits trivially exceed what discovery needs (N agents * up-to-5
391
+ * candidates), and the resulting 429s blocked discovery entirely. This
392
+ * means a malicious customer can publish a fake `payment-completed` to lift
393
+ * an agent's ranking. Acceptable trade-off for devnet / MVP; tighten via
394
+ * recipient-tied checks when the network moves to mainnet with a paid RPC
395
+ * provider.
396
+ */
349
397
  fetchAgents(network?: Network, limit?: number): Promise<Agent[]>;
350
398
  /**
351
399
  * Publish a capability card (kind:31990) as a provider.
@@ -766,6 +814,27 @@ type ParseResult = {
766
814
  */
767
815
  declare function parsePaymentRequest(input: string, options?: ParseOptions): ParseResult;
768
816
 
817
+ /**
818
+ * Lightweight payment verifier used by discovery ranking.
819
+ *
820
+ * Unlike `SolanaPaymentStrategy.verifyPayment`, this is a single-shot check
821
+ * with no retries: discovery cannot afford the 30-second confirmation budget
822
+ * the customer-side verifier uses. If the RPC has not seen the transaction
823
+ * yet, we treat the agent as "no verified paid job" rather than blocking.
824
+ *
825
+ * Positive results are cached forever (Solana txs are immutable once
826
+ * confirmed). Negative results expire after `NEGATIVE_CACHE_TTL_MS` so a
827
+ * just-confirmed tx will be picked up on the next discovery refresh.
828
+ */
829
+ type QuickVerifyReason = 'not_found' | 'tx_failed' | 'recipient_mismatch' | 'rpc_error' | 'invalid_input';
830
+ interface QuickVerifyResult {
831
+ verified: boolean;
832
+ txSignature: string;
833
+ reason?: QuickVerifyReason;
834
+ }
835
+ declare function clearQuickVerifyCache(): void;
836
+ declare function verifyJobPaymentQuick(rpc: Rpc<SolanaRpcApi>, txSignature: string, expectedRecipient: Address): Promise<QuickVerifyResult>;
837
+
769
838
  /**
770
839
  * Snapshot of the on-chain elisym-config program state.
771
840
  *
@@ -952,7 +1021,7 @@ declare function getProtocolProgramId(cluster: ProtocolCluster): Address;
952
1021
  /** Default values for timeouts, retries, and batch sizes. */
953
1022
  declare const DEFAULTS: {
954
1023
  readonly SUBSCRIPTION_TIMEOUT_MS: 120000;
955
- readonly PING_TIMEOUT_MS: 15000;
1024
+ readonly PING_TIMEOUT_MS: 3000;
956
1025
  readonly PING_RETRIES: 2;
957
1026
  readonly PING_CACHE_TTL_MS: 30000;
958
1027
  readonly PAYMENT_EXPIRY_SECS: 600;
@@ -978,4 +1047,4 @@ declare const LIMITS: {
978
1047
  readonly MAX_CAPABILITY_LENGTH: 64;
979
1048
  };
980
1049
 
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 };
1050
+ 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,9 @@ async function createPaymentRequestWithOnchainConfig(rpc, programId, recipient,
994
994
  options
995
995
  );
996
996
  }
997
+ var RANKING_ACTIVITY_WINDOW_SECS = 30 * 24 * 60 * 60;
998
+ var RANKING_BUCKET_SIZE_SECS = 60;
999
+ var COLD_START_BUCKET = -Infinity;
997
1000
  function toDTag(name) {
998
1001
  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
1002
  if (!tag) {
@@ -1001,6 +1004,28 @@ function toDTag(name) {
1001
1004
  }
1002
1005
  return tag;
1003
1006
  }
1007
+ function computeRankKey(agent) {
1008
+ const lastPaidJobAt = agent.lastPaidJobAt ?? 0;
1009
+ const total = agent.totalRatingCount ?? 0;
1010
+ const positive = agent.positiveCount ?? 0;
1011
+ const rate = total > 0 ? positive / total : 0;
1012
+ const bucket = lastPaidJobAt > 0 ? Math.floor(lastPaidJobAt / RANKING_BUCKET_SIZE_SECS) * RANKING_BUCKET_SIZE_SECS : COLD_START_BUCKET;
1013
+ return { bucket, rate, lastPaidJobAt, lastSeen: agent.lastSeen };
1014
+ }
1015
+ function compareAgentsByRank(a, b) {
1016
+ const ka = computeRankKey(a);
1017
+ const kb = computeRankKey(b);
1018
+ if (kb.bucket !== ka.bucket) {
1019
+ return kb.bucket - ka.bucket;
1020
+ }
1021
+ if (kb.rate !== ka.rate) {
1022
+ return kb.rate - ka.rate;
1023
+ }
1024
+ if (kb.lastPaidJobAt !== ka.lastPaidJobAt) {
1025
+ return kb.lastPaidJobAt - ka.lastPaidJobAt;
1026
+ }
1027
+ return kb.lastSeen - ka.lastSeen;
1028
+ }
1004
1029
  function buildAgentsFromEvents(events, network) {
1005
1030
  const latestByDTag = /* @__PURE__ */ new Map();
1006
1031
  for (const event of events) {
@@ -1185,7 +1210,26 @@ var DiscoveryService = class {
1185
1210
  }
1186
1211
  return agents;
1187
1212
  }
1188
- /** Fetch elisym agents filtered by network. */
1213
+ /**
1214
+ * Fetch elisym agents filtered by network, ranked by paid-job recency and
1215
+ * positive-feedback rate.
1216
+ *
1217
+ * Ranking algorithm:
1218
+ * 1. Bucket each agent into 1-minute slots by `lastPaidJobAt` (newest
1219
+ * `payment-completed` feedback timestamp). Cold-start agents go into a
1220
+ * sentinel bucket below all populated buckets.
1221
+ * 2. Within a bucket, sort by positive review rate descending.
1222
+ * 3. Tiebreak by raw `lastPaidJobAt`, then `lastSeen` (NIP-89 freshness).
1223
+ *
1224
+ * NOTE: We trust the `payment-completed` feedback timestamp directly; we do
1225
+ * not verify the embedded `tx` signature on-chain. Public Solana devnet RPC
1226
+ * rate-limits trivially exceed what discovery needs (N agents * up-to-5
1227
+ * candidates), and the resulting 429s blocked discovery entirely. This
1228
+ * means a malicious customer can publish a fake `payment-completed` to lift
1229
+ * an agent's ranking. Acceptable trade-off for devnet / MVP; tighten via
1230
+ * recipient-tied checks when the network moves to mainnet with a paid RPC
1231
+ * provider.
1232
+ */
1189
1233
  async fetchAgents(network = "devnet", limit) {
1190
1234
  const filter = {
1191
1235
  kinds: [KIND_APP_HANDLER],
@@ -1196,40 +1240,78 @@ var DiscoveryService = class {
1196
1240
  }
1197
1241
  const events = await this.pool.querySync(filter);
1198
1242
  const agentMap = buildAgentsFromEvents(events, network);
1199
- const agents = Array.from(agentMap.values()).sort((a, b) => b.lastSeen - a.lastSeen);
1243
+ const agents = Array.from(agentMap.values());
1200
1244
  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
- }
1245
+ if (agentPubkeys.length === 0) {
1246
+ return agents;
1247
+ }
1248
+ const activitySince = Math.floor(Date.now() / 1e3) - RANKING_ACTIVITY_WINDOW_SECS;
1249
+ const resultKinds = /* @__PURE__ */ new Set();
1250
+ for (const agent of agentMap.values()) {
1251
+ for (const k of agent.supportedKinds) {
1252
+ if (k >= KIND_JOB_REQUEST_BASE && k < KIND_JOB_RESULT_BASE) {
1253
+ resultKinds.add(KIND_JOB_RESULT_BASE + (k - KIND_JOB_REQUEST_BASE));
1209
1254
  }
1210
1255
  }
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;
1256
+ }
1257
+ resultKinds.add(jobResultKind(DEFAULT_KIND_OFFSET));
1258
+ const [resultEvents, feedbackEvents] = await Promise.all([
1259
+ this.pool.queryBatched(
1260
+ {
1261
+ kinds: [...resultKinds],
1262
+ since: activitySince
1263
+ },
1264
+ agentPubkeys
1265
+ ),
1266
+ this.pool.queryBatchedByTag(
1267
+ { kinds: [KIND_JOB_FEEDBACK], since: activitySince },
1268
+ "p",
1269
+ agentPubkeys
1270
+ ),
1271
+ this.enrichWithMetadata(agents)
1272
+ ]);
1273
+ for (const ev of resultEvents) {
1274
+ if (!verifyEvent(ev)) {
1275
+ continue;
1276
+ }
1277
+ const agent = agentMap.get(ev.pubkey);
1278
+ if (agent && ev.created_at > agent.lastSeen) {
1279
+ agent.lastSeen = ev.created_at;
1280
+ }
1281
+ }
1282
+ for (const ev of feedbackEvents) {
1283
+ if (!verifyEvent(ev)) {
1284
+ continue;
1285
+ }
1286
+ const targetPubkey = ev.tags.find((t) => t[0] === "p")?.[1];
1287
+ if (!targetPubkey) {
1288
+ continue;
1289
+ }
1290
+ const agent = agentMap.get(targetPubkey);
1291
+ if (!agent) {
1292
+ continue;
1293
+ }
1294
+ if (ev.created_at > agent.lastSeen) {
1295
+ agent.lastSeen = ev.created_at;
1296
+ }
1297
+ const rating = ev.tags.find((t) => t[0] === "rating")?.[1];
1298
+ if (rating === "1" || rating === "0") {
1299
+ agent.totalRatingCount = (agent.totalRatingCount ?? 0) + 1;
1300
+ if (rating === "1") {
1301
+ agent.positiveCount = (agent.positiveCount ?? 0) + 1;
1225
1302
  }
1226
- const agent = agentMap.get(ev.pubkey);
1227
- if (agent && ev.created_at > agent.lastSeen) {
1228
- agent.lastSeen = ev.created_at;
1303
+ }
1304
+ const status = ev.tags.find((t) => t[0] === "status")?.[1];
1305
+ const txTag = ev.tags.find((t) => t[0] === "tx");
1306
+ const txSignature = txTag?.[1];
1307
+ if (status === "payment-completed" && typeof txSignature === "string" && txSignature) {
1308
+ if (!agent.lastPaidJobAt || ev.created_at > agent.lastPaidJobAt) {
1309
+ agent.lastPaidJobAt = ev.created_at;
1310
+ agent.lastPaidJobTx = txSignature;
1229
1311
  }
1230
1312
  }
1231
- agents.sort((a, b) => b.lastSeen - a.lastSeen);
1232
1313
  }
1314
+ agents.sort(compareAgentsByRank);
1233
1315
  return agents;
1234
1316
  }
1235
1317
  /**
@@ -1928,6 +2010,7 @@ var MarketplaceService = class {
1928
2010
  let status = "processing";
1929
2011
  let amount;
1930
2012
  let txHash;
2013
+ let asset;
1931
2014
  if (result) {
1932
2015
  status = "success";
1933
2016
  const amtTag = result.tags.find((t) => t[0] === "amount");
@@ -1936,9 +2019,18 @@ var MarketplaceService = class {
1936
2019
  const allFeedbacksForReq = feedbacksByRequestId.get(req.id) ?? [];
1937
2020
  for (const fb of allFeedbacksForReq) {
1938
2021
  const txTag = fb.tags.find((t) => t[0] === "tx");
1939
- if (txTag?.[1]) {
2022
+ if (txTag?.[1] && !txHash) {
1940
2023
  txHash = txTag[1];
1941
- break;
2024
+ }
2025
+ if (!asset) {
2026
+ const amtTag = fb.tags.find((t) => t[0] === "amount");
2027
+ const requestJson = amtTag?.[2];
2028
+ if (requestJson) {
2029
+ const parsed = parsePaymentRequest(requestJson);
2030
+ if (parsed.ok && parsed.data.asset) {
2031
+ asset = parsed.data.asset;
2032
+ }
2033
+ }
1942
2034
  }
1943
2035
  }
1944
2036
  if (feedback) {
@@ -1967,6 +2059,7 @@ var MarketplaceService = class {
1967
2059
  resultEventId: result?.id,
1968
2060
  amount,
1969
2061
  txHash,
2062
+ asset,
1970
2063
  createdAt: req.created_at
1971
2064
  });
1972
2065
  }
@@ -2705,6 +2798,89 @@ function lamportsToSol(lamports) {
2705
2798
  const frac = lamports % LAMPORTS_PER_SOL2;
2706
2799
  return `${whole}.${frac.toString().padStart(9, "0")}`;
2707
2800
  }
2801
+ var NEGATIVE_CACHE_TTL_MS = 6e4;
2802
+ var verifyCache = /* @__PURE__ */ new Map();
2803
+ function clearQuickVerifyCache() {
2804
+ verifyCache.clear();
2805
+ }
2806
+ async function verifyJobPaymentQuick(rpc, txSignature, expectedRecipient) {
2807
+ if (!txSignature) {
2808
+ return { verified: false, txSignature: "", reason: "invalid_input" };
2809
+ }
2810
+ if (!expectedRecipient || !isAddress(expectedRecipient)) {
2811
+ return { verified: false, txSignature, reason: "invalid_input" };
2812
+ }
2813
+ const cacheKey2 = `${txSignature}:${expectedRecipient}`;
2814
+ const cached = verifyCache.get(cacheKey2);
2815
+ if (cached) {
2816
+ if (cached.result.verified) {
2817
+ return cached.result;
2818
+ }
2819
+ if (Date.now() - cached.cachedAt < NEGATIVE_CACHE_TTL_MS) {
2820
+ return cached.result;
2821
+ }
2822
+ }
2823
+ const result = await doVerifyOnce(rpc, txSignature, expectedRecipient);
2824
+ verifyCache.set(cacheKey2, { result, cachedAt: Date.now() });
2825
+ return result;
2826
+ }
2827
+ async function doVerifyOnce(rpc, txSignature, expectedRecipient) {
2828
+ const sigStr = txSignature;
2829
+ if (!rpc || typeof rpc.getTransaction !== "function") {
2830
+ return { verified: false, txSignature: sigStr, reason: "rpc_error" };
2831
+ }
2832
+ let tx;
2833
+ try {
2834
+ tx = await rpc.getTransaction(txSignature, {
2835
+ commitment: "confirmed",
2836
+ encoding: "json",
2837
+ maxSupportedTransactionVersion: 0
2838
+ }).send();
2839
+ } catch {
2840
+ return { verified: false, txSignature: sigStr, reason: "rpc_error" };
2841
+ }
2842
+ if (!tx) {
2843
+ return { verified: false, txSignature: sigStr, reason: "not_found" };
2844
+ }
2845
+ if (!tx.meta || tx.meta.err) {
2846
+ return { verified: false, txSignature: sigStr, reason: "tx_failed" };
2847
+ }
2848
+ const accountKeys = tx.transaction.message.accountKeys;
2849
+ const recipientStr = expectedRecipient;
2850
+ const recipientIdx = accountKeys.indexOf(recipientStr);
2851
+ if (recipientIdx !== -1) {
2852
+ const preBalances = tx.meta.preBalances;
2853
+ const postBalances = tx.meta.postBalances;
2854
+ if (preBalances && postBalances) {
2855
+ const pre = preBalances[recipientIdx];
2856
+ const post = postBalances[recipientIdx];
2857
+ if (pre !== void 0 && post !== void 0) {
2858
+ const delta = BigInt(post) - BigInt(pre);
2859
+ if (delta > 0n) {
2860
+ return { verified: true, txSignature: sigStr };
2861
+ }
2862
+ }
2863
+ }
2864
+ }
2865
+ const postTokenBalances = tx.meta.postTokenBalances;
2866
+ const preTokenBalances = tx.meta.preTokenBalances;
2867
+ if (postTokenBalances) {
2868
+ for (const post of postTokenBalances) {
2869
+ if (post.owner !== recipientStr) {
2870
+ continue;
2871
+ }
2872
+ const pre = preTokenBalances?.find(
2873
+ (entry) => entry.owner === recipientStr && entry.mint === post.mint
2874
+ );
2875
+ const preAmount = pre ? BigInt(pre.uiTokenAmount.amount) : 0n;
2876
+ const postAmount = BigInt(post.uiTokenAmount.amount);
2877
+ if (postAmount > preAmount) {
2878
+ return { verified: true, txSignature: sigStr };
2879
+ }
2880
+ }
2881
+ }
2882
+ return { verified: false, txSignature: sigStr, reason: "recipient_mismatch" };
2883
+ }
2708
2884
  var SessionSpendLimitEntrySchema = z.object({
2709
2885
  chain: z.enum(["solana"]),
2710
2886
  token: z.string().min(1).max(16).regex(/^[a-z0-9]+$/, "token must be lowercase alphanumeric"),
@@ -2905,6 +3081,6 @@ function makeCensor() {
2905
3081
  };
2906
3082
  }
2907
3083
 
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 };
3084
+ 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
3085
  //# sourceMappingURL=index.js.map
2910
3086
  //# sourceMappingURL=index.js.map