ebay-mcp-remote-edition 3.2.0 → 3.3.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.
@@ -27,7 +27,7 @@ export class MetadataApi {
27
27
  return await this.client.get(`${this.basePath}/marketplace/${marketplaceId}/get_automotive_parts_compatibility_policies`, params);
28
28
  }
29
29
  catch (error) {
30
- throw new Error(`Failed to get automotive parts compatibility policies: ${error instanceof Error ? error.message : 'Unknown error'}`);
30
+ throw new Error(`Failed to get automotive parts compatibility policies: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
31
31
  }
32
32
  }
33
33
  /**
@@ -49,7 +49,7 @@ export class MetadataApi {
49
49
  return await this.client.get(`${this.basePath}/marketplace/${marketplaceId}/get_category_policies`, params);
50
50
  }
51
51
  catch (error) {
52
- throw new Error(`Failed to get category policies: ${error instanceof Error ? error.message : 'Unknown error'}`);
52
+ throw new Error(`Failed to get category policies: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
53
53
  }
54
54
  }
55
55
  /**
@@ -71,7 +71,7 @@ export class MetadataApi {
71
71
  return await this.client.get(`${this.basePath}/marketplace/${marketplaceId}/get_extended_producer_responsibility_policies`, params);
72
72
  }
73
73
  catch (error) {
74
- throw new Error(`Failed to get extended producer responsibility policies: ${error instanceof Error ? error.message : 'Unknown error'}`);
74
+ throw new Error(`Failed to get extended producer responsibility policies: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
75
75
  }
76
76
  }
77
77
  /**
@@ -87,7 +87,7 @@ export class MetadataApi {
87
87
  return await this.client.get(`${this.basePath}/marketplace/${marketplaceId}/get_hazardous_materials_labels`);
88
88
  }
89
89
  catch (error) {
90
- throw new Error(`Failed to get hazardous materials labels: ${error instanceof Error ? error.message : 'Unknown error'}`);
90
+ throw new Error(`Failed to get hazardous materials labels: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
91
91
  }
92
92
  }
93
93
  /**
@@ -109,7 +109,7 @@ export class MetadataApi {
109
109
  return await this.client.get(`${this.basePath}/marketplace/${marketplaceId}/get_item_condition_policies`, params);
110
110
  }
111
111
  catch (error) {
112
- throw new Error(`Failed to get item condition policies: ${error instanceof Error ? error.message : 'Unknown error'}`);
112
+ throw new Error(`Failed to get item condition policies: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
113
113
  }
114
114
  }
115
115
  /**
@@ -131,7 +131,7 @@ export class MetadataApi {
131
131
  return await this.client.get(`${this.basePath}/marketplace/${marketplaceId}/get_listing_structure_policies`, params);
132
132
  }
133
133
  catch (error) {
134
- throw new Error(`Failed to get listing structure policies: ${error instanceof Error ? error.message : 'Unknown error'}`);
134
+ throw new Error(`Failed to get listing structure policies: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
135
135
  }
136
136
  }
137
137
  /**
@@ -153,7 +153,7 @@ export class MetadataApi {
153
153
  return await this.client.get(`${this.basePath}/marketplace/${marketplaceId}/get_negotiated_price_policies`, params);
154
154
  }
155
155
  catch (error) {
156
- throw new Error(`Failed to get negotiated price policies: ${error instanceof Error ? error.message : 'Unknown error'}`);
156
+ throw new Error(`Failed to get negotiated price policies: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
157
157
  }
158
158
  }
159
159
  /**
@@ -169,7 +169,7 @@ export class MetadataApi {
169
169
  return await this.client.get(`${this.basePath}/marketplace/${marketplaceId}/get_product_safety_labels`);
170
170
  }
171
171
  catch (error) {
172
- throw new Error(`Failed to get product safety labels: ${error instanceof Error ? error.message : 'Unknown error'}`);
172
+ throw new Error(`Failed to get product safety labels: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
173
173
  }
174
174
  }
175
175
  /**
@@ -191,7 +191,7 @@ export class MetadataApi {
191
191
  return await this.client.get(`${this.basePath}/marketplace/${marketplaceId}/get_regulatory_policies`, params);
192
192
  }
193
193
  catch (error) {
194
- throw new Error(`Failed to get regulatory policies: ${error instanceof Error ? error.message : 'Unknown error'}`);
194
+ throw new Error(`Failed to get regulatory policies: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
195
195
  }
196
196
  }
197
197
  /**
@@ -213,7 +213,7 @@ export class MetadataApi {
213
213
  return await this.client.get(`${this.basePath}/marketplace/${marketplaceId}/get_return_policies`, params);
214
214
  }
215
215
  catch (error) {
216
- throw new Error(`Failed to get return policies: ${error instanceof Error ? error.message : 'Unknown error'}`);
216
+ throw new Error(`Failed to get return policies: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
217
217
  }
218
218
  }
219
219
  /**
@@ -235,7 +235,7 @@ export class MetadataApi {
235
235
  return await this.client.get(`${this.basePath}/marketplace/${marketplaceId}/get_shipping_cost_type_policies`, params);
236
236
  }
237
237
  catch (error) {
238
- throw new Error(`Failed to get shipping cost type policies: ${error instanceof Error ? error.message : 'Unknown error'}`);
238
+ throw new Error(`Failed to get shipping cost type policies: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
239
239
  }
240
240
  }
241
241
  /**
@@ -257,7 +257,7 @@ export class MetadataApi {
257
257
  return await this.client.get(`${this.basePath}/marketplace/${marketplaceId}/get_classified_ad_policies`, params);
258
258
  }
259
259
  catch (error) {
260
- throw new Error(`Failed to get classified ad policies: ${error instanceof Error ? error.message : 'Unknown error'}`);
260
+ throw new Error(`Failed to get classified ad policies: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
261
261
  }
262
262
  }
263
263
  /**
@@ -273,7 +273,7 @@ export class MetadataApi {
273
273
  return await this.client.get(`${this.basePath}/marketplace/${marketplaceId}/get_currencies`);
274
274
  }
275
275
  catch (error) {
276
- throw new Error(`Failed to get currencies: ${error instanceof Error ? error.message : 'Unknown error'}`);
276
+ throw new Error(`Failed to get currencies: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
277
277
  }
278
278
  }
279
279
  /**
@@ -295,7 +295,7 @@ export class MetadataApi {
295
295
  return await this.client.get(`${this.basePath}/marketplace/${marketplaceId}/get_listing_type_policies`, params);
296
296
  }
297
297
  catch (error) {
298
- throw new Error(`Failed to get listing type policies: ${error instanceof Error ? error.message : 'Unknown error'}`);
298
+ throw new Error(`Failed to get listing type policies: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
299
299
  }
300
300
  }
301
301
  /**
@@ -317,7 +317,7 @@ export class MetadataApi {
317
317
  return await this.client.get(`${this.basePath}/marketplace/${marketplaceId}/get_motors_listing_policies`, params);
318
318
  }
319
319
  catch (error) {
320
- throw new Error(`Failed to get motors listing policies: ${error instanceof Error ? error.message : 'Unknown error'}`);
320
+ throw new Error(`Failed to get motors listing policies: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
321
321
  }
322
322
  }
323
323
  /**
@@ -339,7 +339,7 @@ export class MetadataApi {
339
339
  return await this.client.get(`${this.basePath}/marketplace/${marketplaceId}/get_shipping_policies`, params);
340
340
  }
341
341
  catch (error) {
342
- throw new Error(`Failed to get shipping policies: ${error instanceof Error ? error.message : 'Unknown error'}`);
342
+ throw new Error(`Failed to get shipping policies: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
343
343
  }
344
344
  }
345
345
  /**
@@ -361,7 +361,7 @@ export class MetadataApi {
361
361
  return await this.client.get(`${this.basePath}/marketplace/${marketplaceId}/get_site_visibility_policies`, params);
362
362
  }
363
363
  catch (error) {
364
- throw new Error(`Failed to get site visibility policies: ${error instanceof Error ? error.message : 'Unknown error'}`);
364
+ throw new Error(`Failed to get site visibility policies: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
365
365
  }
366
366
  }
367
367
  /**
@@ -377,7 +377,7 @@ export class MetadataApi {
377
377
  return await this.client.post(`${this.basePath}/compatibilities/get_compatibilities_by_specification`, specification);
378
378
  }
379
379
  catch (error) {
380
- throw new Error(`Failed to get compatibilities by specification: ${error instanceof Error ? error.message : 'Unknown error'}`);
380
+ throw new Error(`Failed to get compatibilities by specification: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
381
381
  }
382
382
  }
383
383
  /**
@@ -393,7 +393,7 @@ export class MetadataApi {
393
393
  return await this.client.post(`${this.basePath}/compatibilities/get_compatibility_property_names`, data);
394
394
  }
395
395
  catch (error) {
396
- throw new Error(`Failed to get compatibility property names: ${error instanceof Error ? error.message : 'Unknown error'}`);
396
+ throw new Error(`Failed to get compatibility property names: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
397
397
  }
398
398
  }
399
399
  /**
@@ -409,7 +409,7 @@ export class MetadataApi {
409
409
  return await this.client.post(`${this.basePath}/compatibilities/get_compatibility_property_values`, data);
410
410
  }
411
411
  catch (error) {
412
- throw new Error(`Failed to get compatibility property values: ${error instanceof Error ? error.message : 'Unknown error'}`);
412
+ throw new Error(`Failed to get compatibility property values: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
413
413
  }
414
414
  }
415
415
  /**
@@ -425,7 +425,7 @@ export class MetadataApi {
425
425
  return await this.client.post(`${this.basePath}/compatibilities/get_multi_compatibility_property_values`, data);
426
426
  }
427
427
  catch (error) {
428
- throw new Error(`Failed to get multi compatibility property values: ${error instanceof Error ? error.message : 'Unknown error'}`);
428
+ throw new Error(`Failed to get multi compatibility property values: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
429
429
  }
430
430
  }
431
431
  /**
@@ -441,7 +441,7 @@ export class MetadataApi {
441
441
  return await this.client.post(`${this.basePath}/compatibilities/get_product_compatibilities`, data);
442
442
  }
443
443
  catch (error) {
444
- throw new Error(`Failed to get product compatibilities: ${error instanceof Error ? error.message : 'Unknown error'}`);
444
+ throw new Error(`Failed to get product compatibilities: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
445
445
  }
446
446
  }
447
447
  /**
@@ -457,7 +457,7 @@ export class MetadataApi {
457
457
  return await this.client.get(`${this.basePath}/country/${countryCode}/sales_tax_jurisdiction`);
458
458
  }
459
459
  catch (error) {
460
- throw new Error(`Failed to get sales tax jurisdictions: ${error instanceof Error ? error.message : 'Unknown error'}`);
460
+ throw new Error(`Failed to get sales tax jurisdictions: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
461
461
  }
462
462
  }
463
463
  /**
@@ -479,7 +479,7 @@ export class MetadataApi {
479
479
  return await this.client.get(`${this.basePath}/marketplace/${marketplaceId}/get_product_compliance_policies`, params);
480
480
  }
481
481
  catch (error) {
482
- throw new Error(`Failed to get product compliance policies: ${error instanceof Error ? error.message : 'Unknown error'}`);
482
+ throw new Error(`Failed to get product compliance policies: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
483
483
  }
484
484
  }
485
485
  }
@@ -165,10 +165,10 @@ export class EbayOAuthClient {
165
165
  if (axios.isAxiosError(error) && error.response?.data) {
166
166
  const data = error.response.data;
167
167
  if (data.error_description) {
168
- throw new Error(data.error_description);
168
+ throw new Error(data.error_description, { cause: error });
169
169
  }
170
170
  if (data.error) {
171
- throw new Error(data.error);
171
+ throw new Error(data.error, { cause: error });
172
172
  }
173
173
  }
174
174
  throw error;
@@ -20,7 +20,7 @@ export class TokenVerifier {
20
20
  this.metadata = response.data;
21
21
  }
22
22
  catch (error) {
23
- throw new Error(`Failed to load OAuth server metadata: ${error instanceof Error ? error.message : 'Unknown error'}`);
23
+ throw new Error(`Failed to load OAuth server metadata: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
24
24
  }
25
25
  }
26
26
  else {
@@ -104,7 +104,7 @@ export class TokenVerifier {
104
104
  }
105
105
  catch (error) {
106
106
  if (axios.isAxiosError(error)) {
107
- throw new Error(`Token introspection failed: ${error.response?.data?.error_description || error.message}`);
107
+ throw new Error(`Token introspection failed: ${error.response?.data?.error_description || error.message}`, { cause: error });
108
108
  }
109
109
  throw error;
110
110
  }
@@ -150,7 +150,7 @@ export class TokenVerifier {
150
150
  };
151
151
  }
152
152
  catch (error) {
153
- throw new Error(`JWT verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
153
+ throw new Error(`JWT verification failed: ${error instanceof Error ? error.message : 'Unknown error'}`, { cause: error });
154
154
  }
155
155
  }
156
156
  /**
@@ -429,6 +429,11 @@ function mountEnvRouter(hardcodedEnv, serverUrl, iconBaseUrl) {
429
429
  const storedTokens = validationRunnerUserId
430
430
  ? await authStore.getUserTokens(validationRunnerUserId, environment)
431
431
  : null;
432
+ const socialConfig = {
433
+ hasTwitterBearerToken: Boolean(process.env.TWITTER_BEARER_TOKEN?.trim()),
434
+ hasYoutubeApiKey: Boolean(process.env.YOUTUBE_API_KEY?.trim()),
435
+ hasRedditUserAgent: Boolean(process.env.REDDIT_USER_AGENT?.trim()),
436
+ };
432
437
  let authenticated = false;
433
438
  let authError = null;
434
439
  let tokenStatus = null;
@@ -467,7 +472,7 @@ function mountEnvRouter(hardcodedEnv, serverUrl, iconBaseUrl) {
467
472
  });
468
473
  }
469
474
  }
470
- res.json({
475
+ const healthResponse = {
471
476
  status: authenticated ? 'ok' : 'degraded',
472
477
  environment,
473
478
  validationRunnerUserId,
@@ -479,9 +484,21 @@ function mountEnvRouter(hardcodedEnv, serverUrl, iconBaseUrl) {
479
484
  ebay: { available: true, implemented: true, confidence: 'medium' },
480
485
  social: { available: false, implemented: false, confidence: 'low' },
481
486
  chart: { available: false, implemented: false, confidence: 'low' },
487
+ socialConfig,
482
488
  },
483
489
  ...(authError ? { authError } : {}),
490
+ };
491
+ serverLogger.info('Validation health response emitted', {
492
+ environment,
493
+ path: req.originalUrl,
494
+ status: healthResponse.status,
495
+ version: getVersion(),
496
+ hasSocialConfigAtRoot: Object.prototype.hasOwnProperty.call(healthResponse, 'socialConfig'),
497
+ hasSocialConfigUnderProviders: Object.prototype.hasOwnProperty.call(healthResponse.providers, 'socialConfig'),
498
+ providerKeys: Object.keys(healthResponse.providers),
499
+ socialConfig,
484
500
  });
501
+ res.json(healthResponse);
485
502
  });
486
503
  // ── RFC 8414 – Authorization Server Metadata ──────────────────────────
487
504
  // For env-scoped routers: endpoints are relative to the env base URL.
@@ -196,7 +196,7 @@ export async function executeTool(api, toolName, args) {
196
196
  };
197
197
  }
198
198
  catch (error) {
199
- throw new Error(`Failed to convert date: ${error instanceof Error ? error.message : String(error)}`);
199
+ throw new Error(`Failed to convert date: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
200
200
  }
201
201
  }
202
202
  case 'ebay_validate_token_expiry': {
@@ -217,7 +217,7 @@ export async function executeTool(api, toolName, args) {
217
217
  };
218
218
  }
219
219
  catch (error) {
220
- throw new Error(`Failed to validate token expiry: ${error instanceof Error ? error.message : String(error)}`);
220
+ throw new Error(`Failed to validate token expiry: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
221
221
  }
222
222
  }
223
223
  case 'ebay_set_user_tokens_with_expiry': {
@@ -272,7 +272,7 @@ export async function executeTool(api, toolName, args) {
272
272
  };
273
273
  }
274
274
  catch (error) {
275
- throw new Error(`Failed to set user tokens: ${error instanceof Error ? error.message : String(error)}`);
275
+ throw new Error(`Failed to set user tokens: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
276
276
  }
277
277
  }
278
278
  case 'ebay_display_credentials': {
@@ -370,7 +370,7 @@ export async function executeTool(api, toolName, args) {
370
370
  };
371
371
  }
372
372
  catch (error) {
373
- throw new Error(`Failed to exchange authorization code: ${error instanceof Error ? error.message : String(error)}`);
373
+ throw new Error(`Failed to exchange authorization code: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
374
374
  }
375
375
  }
376
376
  case 'ebay_refresh_access_token': {
@@ -401,7 +401,7 @@ export async function executeTool(api, toolName, args) {
401
401
  };
402
402
  }
403
403
  catch (error) {
404
- throw new Error(`Failed to refresh access token: ${error instanceof Error ? error.message : String(error)}`);
404
+ throw new Error(`Failed to refresh access token: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
405
405
  }
406
406
  }
407
407
  // Account Management
@@ -0,0 +1,77 @@
1
+ function sanitizeText(value) {
2
+ if (typeof value !== 'string') {
3
+ return null;
4
+ }
5
+ const trimmed = value.trim();
6
+ return trimmed.length > 0 ? trimmed : null;
7
+ }
8
+ function buildCompactText(...parts) {
9
+ const compact = parts
10
+ .map((part) => sanitizeText(part))
11
+ .filter((part) => part !== null)
12
+ .join(' ')
13
+ .replace(/\s+/g, ' ')
14
+ .trim();
15
+ return compact.length > 0 ? compact : null;
16
+ }
17
+ function getQueryContext(request) {
18
+ return request.validation.queryContext;
19
+ }
20
+ function getFallbackArtist(request) {
21
+ return sanitizeText(request.item.canonicalArtists[0]);
22
+ }
23
+ function getFallbackAlbum(request) {
24
+ return sanitizeText(request.item.relatedAlbums[0]);
25
+ }
26
+ function getFallbackItem(request) {
27
+ return sanitizeText(request.item.name);
28
+ }
29
+ export function buildValidationEffectiveContext(request) {
30
+ const queryContext = getQueryContext(request);
31
+ const sourceType = request.sourceContext?.sourceType ?? 'item';
32
+ const searchArtist = sanitizeText(queryContext?.resolvedSearchArtist) ?? getFallbackArtist(request);
33
+ const searchAlbum = sanitizeText(getFallbackAlbum(request));
34
+ const searchItem = sanitizeText(queryContext?.resolvedSearchItem) ??
35
+ (sourceType === 'item' ? getFallbackItem(request) : null);
36
+ const searchEvent = sanitizeText(queryContext?.resolvedSearchEvent);
37
+ const searchLocation = sanitizeText(queryContext?.resolvedSearchLocation);
38
+ const resolvedSearchQuery = sanitizeText(queryContext?.resolvedSearchQuery);
39
+ const itemRecordId = sanitizeText(request.item.recordId);
40
+ const eventRecordId = sanitizeText(request.sourceContext?.eventRecordId);
41
+ const itemName = getFallbackItem(request);
42
+ const hasItem = sourceType === 'item'
43
+ ? (request.sourceContext?.hasItem ?? itemRecordId !== null) || itemName !== null
44
+ : request.sourceContext?.hasItem === true || itemRecordId !== null;
45
+ const hasEvent = sourceType === 'event'
46
+ ? true
47
+ : request.sourceContext?.hasEvent === true || searchEvent !== null || eventRecordId !== null;
48
+ const effectiveSearchQuery = resolvedSearchQuery ??
49
+ (sourceType === 'event'
50
+ ? buildCompactText(searchArtist, searchEvent, searchItem, searchLocation)
51
+ : buildCompactText(searchArtist, searchAlbum ?? searchItem, searchLocation));
52
+ return {
53
+ sourceType,
54
+ mode: sourceType,
55
+ validationScope: sanitizeText(queryContext?.validationScope),
56
+ queryScope: sanitizeText(queryContext?.queryScope),
57
+ directQueryActive: queryContext?.directQueryActive === true,
58
+ resolvedSearchQuery,
59
+ effectiveSearchQuery,
60
+ searchArtist,
61
+ searchAlbum,
62
+ searchItem,
63
+ searchEvent,
64
+ searchLocation,
65
+ hasItem,
66
+ hasEvent,
67
+ itemRecordId,
68
+ eventRecordId,
69
+ itemName,
70
+ eventDate: sanitizeText(request.item.releaseDate),
71
+ dDay: request.validation.dDay,
72
+ requestTimestamp: request.timestamp,
73
+ };
74
+ }
75
+ export function getValidationEffectiveContext(request) {
76
+ return request.effectiveContext ?? buildValidationEffectiveContext(request);
77
+ }
@@ -1,4 +1,5 @@
1
1
  import axios from 'axios';
2
+ import { buildResolvedSoldQueryPlan } from './query-utils.js';
2
3
  function round(value) {
3
4
  return Math.round(value * 100) / 100;
4
5
  }
@@ -14,23 +15,6 @@ function normalizePrice(value) {
14
15
  }
15
16
  return null;
16
17
  }
17
- function buildSoldKeywords(request) {
18
- const parts = new Set();
19
- const push = (values) => {
20
- for (const value of values) {
21
- const normalized = value.trim();
22
- if (normalized) {
23
- parts.add(normalized);
24
- }
25
- }
26
- };
27
- push([request.item.name]);
28
- push(request.item.canonicalArtists.slice(0, 2));
29
- push(request.item.relatedAlbums.slice(0, 1));
30
- push(request.item.variation.slice(0, 2));
31
- push([request.validation.validationType]);
32
- return Array.from(parts).join(' ').replace(/\s+/g, ' ').trim();
33
- }
34
18
  function parseSoldDate(value) {
35
19
  if (!value) {
36
20
  return null;
@@ -96,7 +80,7 @@ function scoreSoldConfidence(soldResultsCount, soldItemsSample) {
96
80
  }
97
81
  return 'Low';
98
82
  }
99
- function createEmptySoldSignals(query, status, errorMessage) {
83
+ function createEmptySoldSignals(query, queryCandidates = [], status, errorMessage) {
100
84
  return {
101
85
  provider: 'third_party_sold_api',
102
86
  confidence: 'Low',
@@ -115,6 +99,9 @@ function createEmptySoldSignals(query, status, errorMessage) {
115
99
  daysTracked: null,
116
100
  },
117
101
  query,
102
+ queryCandidates,
103
+ selectedQuery: query ?? undefined,
104
+ selectedQueryTier: query ? 1 : null,
118
105
  responseUrl: null,
119
106
  status,
120
107
  ...(errorMessage ? { errorMessage } : {}),
@@ -123,49 +110,107 @@ function createEmptySoldSignals(query, status, errorMessage) {
123
110
  export async function getEbaySoldValidationSignals(request) {
124
111
  const soldApiUrl = process.env.SOLD_ITEMS_API_URL?.trim();
125
112
  const soldApiKey = process.env.SOLD_ITEMS_API_KEY?.trim();
126
- const query = buildSoldKeywords(request);
113
+ const { queryPlan, queryResolution } = buildResolvedSoldQueryPlan(request);
114
+ const queryCandidates = queryPlan.map((candidate) => candidate.query);
115
+ const query = queryCandidates[0] ?? null;
116
+ const queryDiagnostics = [];
127
117
  if (!soldApiUrl || !soldApiKey || !query) {
128
- return createEmptySoldSignals(query, 'unavailable');
118
+ return {
119
+ ...createEmptySoldSignals(query, queryCandidates, 'unavailable'),
120
+ queryDiagnostics,
121
+ queryResolution,
122
+ };
129
123
  }
130
124
  try {
131
125
  const endpoint = soldApiUrl.endsWith('/findCompletedItems')
132
126
  ? soldApiUrl
133
127
  : `${soldApiUrl.replace(/\/$/, '')}/findCompletedItems`;
134
128
  const host = new URL(endpoint).host;
135
- const response = await axios.post(endpoint, {
136
- keywords: query,
137
- excluded_keywords: 'set lot bundle photocard fanmade replica unofficial',
138
- max_search_results: 120,
139
- remove_outliers: true,
140
- site_id: '0',
141
- }, {
142
- timeout: 30000,
143
- headers: {
144
- 'Content-Type': 'application/json',
145
- 'x-rapidapi-key': soldApiKey,
146
- 'x-rapidapi-host': host,
147
- },
148
- });
149
- const data = response.data;
150
- const soldItemsSample = normalizeProducts(data.products);
151
- const soldVelocity = bucketSoldVelocity(soldItemsSample, request.timestamp);
152
- const soldResultsCount = typeof data.results === 'number' && Number.isFinite(data.results) ? data.results : null;
129
+ let selectedResult = {
130
+ ...createEmptySoldSignals(query, queryCandidates, 'unavailable'),
131
+ queryDiagnostics,
132
+ queryResolution,
133
+ };
134
+ let lastErrorMessage;
135
+ for (const [index, candidate] of queryCandidates.entries()) {
136
+ try {
137
+ const response = await axios.post(endpoint, {
138
+ keywords: candidate,
139
+ excluded_keywords: 'set lot bundle photocard fanmade replica unofficial',
140
+ max_search_results: 120,
141
+ remove_outliers: true,
142
+ site_id: '0',
143
+ }, {
144
+ timeout: 30000,
145
+ headers: {
146
+ 'Content-Type': 'application/json',
147
+ 'x-rapidapi-key': soldApiKey,
148
+ 'x-rapidapi-host': host,
149
+ },
150
+ });
151
+ const data = response.data;
152
+ const soldItemsSample = normalizeProducts(data.products);
153
+ const soldVelocity = bucketSoldVelocity(soldItemsSample, request.timestamp);
154
+ const soldResultsCount = typeof data.results === 'number' && Number.isFinite(data.results) ? data.results : null;
155
+ queryDiagnostics.push({
156
+ query: candidate,
157
+ tier: index + 1,
158
+ family: queryPlan[index]?.family,
159
+ soldResultsCount,
160
+ status: data.success === false ? 'error' : 'ok',
161
+ });
162
+ selectedResult = {
163
+ provider: 'third_party_sold_api',
164
+ confidence: scoreSoldConfidence(soldResultsCount, soldItemsSample),
165
+ soldResultsCount,
166
+ soldAveragePriceUsd: normalizePrice(data.average_price),
167
+ soldMedianPriceUsd: normalizePrice(data.median_price),
168
+ soldMinPriceUsd: normalizePrice(data.min_price),
169
+ soldMaxPriceUsd: normalizePrice(data.max_price),
170
+ soldItemsSample,
171
+ soldVelocity,
172
+ query: candidate,
173
+ queryCandidates,
174
+ queryDiagnostics: [...queryDiagnostics],
175
+ selectedQuery: candidate,
176
+ selectedQueryTier: index + 1,
177
+ responseUrl: typeof data.response_url === 'string' ? data.response_url : null,
178
+ status: data.success === false ? 'error' : 'ok',
179
+ queryResolution,
180
+ };
181
+ if ((soldResultsCount ?? 0) >= 5) {
182
+ break;
183
+ }
184
+ }
185
+ catch (error) {
186
+ lastErrorMessage = error instanceof Error ? error.message : String(error);
187
+ queryDiagnostics.push({
188
+ query: candidate,
189
+ tier: index + 1,
190
+ family: queryPlan[index]?.family,
191
+ soldResultsCount: null,
192
+ status: 'error',
193
+ note: lastErrorMessage,
194
+ });
195
+ }
196
+ }
197
+ if (selectedResult.status === 'unavailable' && queryDiagnostics.length > 0) {
198
+ return {
199
+ ...createEmptySoldSignals(query, queryCandidates, 'error', lastErrorMessage),
200
+ queryDiagnostics,
201
+ queryResolution,
202
+ };
203
+ }
153
204
  return {
154
- provider: 'third_party_sold_api',
155
- confidence: scoreSoldConfidence(soldResultsCount, soldItemsSample),
156
- soldResultsCount,
157
- soldAveragePriceUsd: normalizePrice(data.average_price),
158
- soldMedianPriceUsd: normalizePrice(data.median_price),
159
- soldMinPriceUsd: normalizePrice(data.min_price),
160
- soldMaxPriceUsd: normalizePrice(data.max_price),
161
- soldItemsSample,
162
- soldVelocity,
163
- query,
164
- responseUrl: typeof data.response_url === 'string' ? data.response_url : null,
165
- status: data.success === false ? 'error' : 'ok',
205
+ ...selectedResult,
206
+ queryResolution,
166
207
  };
167
208
  }
168
209
  catch (error) {
169
- return createEmptySoldSignals(query, 'error', error instanceof Error ? error.message : String(error));
210
+ return {
211
+ ...createEmptySoldSignals(query, queryCandidates, 'error', error instanceof Error ? error.message : String(error)),
212
+ queryDiagnostics,
213
+ queryResolution,
214
+ };
170
215
  }
171
216
  }