diff --git a/EbayListingTool/Services/EbayAuthService.cs b/EbayListingTool/Services/EbayAuthService.cs
index f0167b2..68a9e6c 100644
--- a/EbayListingTool/Services/EbayAuthService.cs
+++ b/EbayListingTool/Services/EbayAuthService.cs
@@ -15,6 +15,10 @@ public class EbayAuthService
private readonly string _tokenPath;
private EbayToken? _token;
+ // App-level (client credentials) token — no user login needed, used for Browse API
+ private string? _appToken;
+ private DateTime _appTokenExpiry = DateTime.MinValue;
+
private static readonly string[] Scopes =
[
"https://api.ebay.com/oauth/api_scope",
@@ -191,6 +195,47 @@ public class EbayAuthService
}
}
+ ///
+ /// Returns an application-level OAuth token (client credentials grant).
+ /// Does NOT require the user to be logged in — uses Client ID + Secret only.
+ /// Cached for the token lifetime minus a 5-minute safety margin.
+ ///
+ public async Task GetAppTokenAsync()
+ {
+ if (_appToken != null && DateTime.UtcNow < _appTokenExpiry)
+ return _appToken;
+
+ if (string.IsNullOrEmpty(_settings.ClientId) || string.IsNullOrEmpty(_settings.ClientSecret))
+ throw new InvalidOperationException("eBay Client ID / Secret not configured in appsettings.json.");
+
+ var tokenUrl = _settings.Sandbox
+ ? "https://api.sandbox.ebay.com/identity/v1/oauth2/token"
+ : "https://api.ebay.com/identity/v1/oauth2/token";
+
+ using var http = new HttpClient();
+ var credentials = Convert.ToBase64String(
+ Encoding.UTF8.GetBytes($"{_settings.ClientId}:{_settings.ClientSecret}"));
+ http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", credentials);
+
+ var body = new FormUrlEncodedContent(new Dictionary
+ {
+ ["grant_type"] = "client_credentials",
+ ["scope"] = "https://api.ebay.com/oauth/api_scope"
+ });
+
+ var response = await http.PostAsync(tokenUrl, body);
+ var json = await response.Content.ReadAsStringAsync();
+
+ if (!response.IsSuccessStatusCode)
+ throw new HttpRequestException($"App token request failed: {json}");
+
+ var obj = JObject.Parse(json);
+ _appToken = obj["access_token"]!.ToString();
+ var expiresIn = obj["expires_in"]?.Value() ?? 7200;
+ _appTokenExpiry = DateTime.UtcNow.AddSeconds(expiresIn - 300); // 5-min buffer
+ return _appToken;
+ }
+
public void Disconnect()
{
_token = null;
diff --git a/EbayListingTool/Services/EbayPriceResearchService.cs b/EbayListingTool/Services/EbayPriceResearchService.cs
new file mode 100644
index 0000000..f45095f
--- /dev/null
+++ b/EbayListingTool/Services/EbayPriceResearchService.cs
@@ -0,0 +1,80 @@
+using System.Net.Http.Headers;
+using Newtonsoft.Json.Linq;
+
+namespace EbayListingTool.Services;
+
+public class LivePriceResult
+{
+ public List Prices { get; init; } = new();
+ public int Count => Prices.Count;
+ public decimal Min => Prices.Count > 0 ? Prices.Min() : 0;
+ public decimal Max => Prices.Count > 0 ? Prices.Max() : 0;
+ public decimal Median => Prices.Count > 0 ? Percentile(50) : 0;
+ public decimal Suggested => Prices.Count > 0 ? Percentile(40) : 0; // 40th pct: competitive but not lowest
+
+ private decimal Percentile(int pct)
+ {
+ var sorted = Prices.OrderBy(p => p).ToList();
+ int idx = (int)Math.Round((pct / 100.0) * (sorted.Count - 1));
+ return sorted[Math.Clamp(idx, 0, sorted.Count - 1)];
+ }
+}
+
+///
+/// Uses the eBay Browse API (app-level token, no user login required) to fetch
+/// current Buy It Now prices for similar items on eBay UK.
+///
+public class EbayPriceResearchService
+{
+ private readonly EbayAuthService _auth;
+ private static readonly HttpClient _http = new();
+
+ public EbayPriceResearchService(EbayAuthService auth)
+ {
+ _auth = auth;
+ }
+
+ public async Task GetLivePricesAsync(string query, int limit = 20)
+ {
+ if (string.IsNullOrWhiteSpace(query))
+ return new LivePriceResult();
+
+ var token = await _auth.GetAppTokenAsync();
+ var baseUrl = _auth.BaseUrl;
+
+ using var request = new HttpRequestMessage(HttpMethod.Get,
+ $"{baseUrl}/buy/browse/v1/item_summary/search" +
+ $"?q={Uri.EscapeDataString(query)}" +
+ $"&filter=buyingOptions%3A%7BFIXED_PRICE%7D" +
+ $"&limit={limit}" +
+ $"&sort=price");
+
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
+ request.Headers.Add("X-EBAY-C-MARKETPLACE-ID", "EBAY_GB");
+
+ var response = await _http.SendAsync(request);
+ var json = await response.Content.ReadAsStringAsync();
+
+ if (!response.IsSuccessStatusCode)
+ throw new HttpRequestException($"eBay Browse API error ({(int)response.StatusCode}): {json}");
+
+ var obj = JObject.Parse(json);
+ var items = obj["itemSummaries"] as JArray ?? new JArray();
+ var prices = new List();
+
+ foreach (var item in items)
+ {
+ var priceVal = item["price"]?["value"]?.ToString();
+ var currency = item["price"]?["currency"]?.ToString();
+ if (currency == "GBP" && decimal.TryParse(priceVal,
+ System.Globalization.NumberStyles.Any,
+ System.Globalization.CultureInfo.InvariantCulture,
+ out var p) && p > 0)
+ {
+ prices.Add(p);
+ }
+ }
+
+ return new LivePriceResult { Prices = prices };
+ }
+}
diff --git a/EbayListingTool/Views/MainWindow.xaml.cs b/EbayListingTool/Views/MainWindow.xaml.cs
index 375dcf4..6e4bde5 100644
--- a/EbayListingTool/Views/MainWindow.xaml.cs
+++ b/EbayListingTool/Views/MainWindow.xaml.cs
@@ -14,6 +14,7 @@ public partial class MainWindow : MetroWindow
private readonly AiAssistantService _aiService;
private readonly BulkImportService _bulkService;
private readonly SavedListingsService _savedService;
+ private readonly EbayPriceResearchService _priceService;
public MainWindow()
{
@@ -26,9 +27,10 @@ public partial class MainWindow : MetroWindow
_aiService = new AiAssistantService(config);
_bulkService = new BulkImportService();
_savedService = new SavedListingsService();
+ _priceService = new EbayPriceResearchService(_auth);
// Photo Analysis tab — no eBay needed
- PhotoView.Initialise(_aiService, _savedService);
+ PhotoView.Initialise(_aiService, _savedService, _priceService);
PhotoView.UseDetailsRequested += OnUseDetailsRequested;
// Saved Listings tab
diff --git a/EbayListingTool/Views/PhotoAnalysisView.xaml b/EbayListingTool/Views/PhotoAnalysisView.xaml
index af741cc..7c24000 100644
--- a/EbayListingTool/Views/PhotoAnalysisView.xaml
+++ b/EbayListingTool/Views/PhotoAnalysisView.xaml
@@ -490,6 +490,18 @@
StringFormat="F2" Interval="0.5"/>
+
+
+
+
+
+
_currentImagePaths = new();
private PhotoAnalysisResult? _lastResult;
private int _activePhotoIndex = 0;
@@ -50,10 +51,12 @@ public partial class PhotoAnalysisView : UserControl
PhotoPreviewContainer.SizeChanged += PhotoPreviewContainer_SizeChanged;
}
- public void Initialise(AiAssistantService aiService, SavedListingsService savedService)
+ public void Initialise(AiAssistantService aiService, SavedListingsService savedService,
+ EbayPriceResearchService priceService)
{
_aiService = aiService;
_savedService = savedService;
+ _priceService = priceService;
}
// ---- Photo clip geometry sync ----
@@ -204,6 +207,8 @@ public partial class PhotoAnalysisView : UserControl
var result = await _aiService.AnalyseItemFromPhotosAsync(_currentImagePaths);
_lastResult = result;
ShowResults(result);
+ // Fire live price lookup in background — updates price display when ready
+ _ = UpdateLivePricesAsync(result.Title);
}
catch (Exception ex)
{
@@ -344,11 +349,58 @@ public partial class PhotoAnalysisView : UserControl
TitleBox.Text = r.Title;
DescriptionBox.Text = r.Description;
+ // Reset live price row until lookup completes
+ LivePriceRow.Visibility = Visibility.Collapsed;
+
// Animate results in
var sb = (Storyboard)FindResource("ResultsReveal");
sb.Begin(this);
}
+ private async Task UpdateLivePricesAsync(string query)
+ {
+ if (_priceService == null) return;
+
+ // Show spinner
+ LivePriceRow.Visibility = Visibility.Visible;
+ LivePriceSpinner.Visibility = Visibility.Visible;
+ LivePriceStatus.Text = "Checking live eBay UK prices…";
+
+ try
+ {
+ var live = await _priceService.GetLivePricesAsync(query);
+
+ if (live.Count == 0)
+ {
+ LivePriceStatus.Text = "No matching live listings found.";
+ LivePriceSpinner.Visibility = Visibility.Collapsed;
+ return;
+ }
+
+ // Update range bar with real data
+ PriceMinText.Text = $"£{live.Min:F2}";
+ PriceMaxText.Text = $"£{live.Max:F2}";
+ PriceRangeBar.Visibility = Visibility.Visible;
+
+ // Update suggested price to 40th percentile (competitive but not cheapest)
+ var suggested = live.Suggested;
+ PriceSuggestedText.Text = $"£{suggested:F2}";
+ PriceOverride.Value = (double)suggested;
+ if (_lastResult != null) _lastResult.PriceSuggested = suggested;
+
+ // Update status label
+ LivePriceSpinner.Visibility = Visibility.Collapsed;
+ LivePriceStatus.Text =
+ $"Based on {live.Count} live eBay UK listing{(live.Count == 1 ? "" : "s")} " +
+ $"(range £{live.Min:F2} – £{live.Max:F2})";
+ }
+ catch (Exception ex)
+ {
+ LivePriceSpinner.Visibility = Visibility.Collapsed;
+ LivePriceStatus.Text = $"Live price lookup unavailable: {ex.Message}";
+ }
+ }
+
private void TitleBox_TextChanged(object sender, TextChangedEventArgs e)
{
var len = TitleBox.Text.Length;