[object Object]

← back to AbramsEgo

spend-reviews: short product names in drafts + robust Amazon ingest

f7369eb515f212eaf0fd84926ff1e22cfd03ee28 · 2026-09-11 09:58:53 -0700 · Steve

- drafts.js: shortProduct() trims Amazon's 200-char keyword-soup titles to a natural short name (used in review title/body + seller email), so drafts read human not robotic
- amazon.js: point at current /your-orders/orders URL + add settle-retry loop (poll until the SPA renders order cards) — fixes the evaluate-before-render race that scraped 0

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit f7369eb515f212eaf0fd84926ff1e22cfd03ee28
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Sep 11 09:58:53 2026 -0700

    spend-reviews: short product names in drafts + robust Amazon ingest
    
    - drafts.js: shortProduct() trims Amazon's 200-char keyword-soup titles to a natural short name (used in review title/body + seller email), so drafts read human not robotic
    - amazon.js: point at current /your-orders/orders URL + add settle-retry loop (poll until the SPA renders order cards) — fixes the evaluate-before-render race that scraped 0
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 lib/spend-reviews/adapters/amazon.js | 18 +++++++++++++-----
 lib/spend-reviews/drafts.js          | 19 ++++++++++++++++---
 2 files changed, 29 insertions(+), 8 deletions(-)

diff --git a/lib/spend-reviews/adapters/amazon.js b/lib/spend-reviews/adapters/amazon.js
index 24aeeada..ef941bf6 100644
--- a/lib/spend-reviews/adapters/amazon.js
+++ b/lib/spend-reviews/adapters/amazon.js
@@ -13,7 +13,7 @@
 const { execFile } = require('child_process');
 const cfg = require('../config');
 
-const ORDERS_URL = 'https://www.amazon.com/gp/css/order-history';
+const ORDERS_URL = 'https://www.amazon.com/your-orders/orders';
 
 function openclaw(args, timeoutMs = 45000) {
   return new Promise((resolve) => {
@@ -96,14 +96,22 @@ async function ingest(opts) {
     return { ok: false, items: [], meta: { source: 'amazon', needsAction: 'openclaw-open-failed',
       note: 'openclaw could not open the order-history page.', detail: open.stderr } };
   }
-  // 3. scrape via evaluate
-  const evalRes = await openclaw(['browser', 'evaluate', '--fn', SCRAPE_FN], 45000);
-  const data = extractJson(evalRes.stdout);
+  // 3. scrape via evaluate — the orders page is an SPA that renders cards AFTER a
+  // redirect + async load, so poll a few times (settle) before giving up on an
+  // empty read. Stop early once we see the login wall OR real orders.
+  let data = null; let lastRaw = '';
+  for (let attempt = 0; attempt < 6; attempt++) {
+    await new Promise((r) => setTimeout(r, attempt === 0 ? 2500 : 2000));
+    const evalRes = await openclaw(['browser', 'evaluate', '--fn', SCRAPE_FN], 45000);
+    lastRaw = evalRes.stdout || '';
+    data = extractJson(lastRaw);
+    if (data && (data.login || (data.orders && data.orders.length))) break;
+  }
   if (!data) {
     // degrade: could not read structured data (markup changed / eval blocked)
     return { ok: true, items: [], meta: { source: 'amazon',
       note: 'opened order history but could not read structured orders (Amazon DOM may have changed) — adjust SCRAPE_FN selectors.',
-      raw: (evalRes.stdout || '').slice(0, 400) } };
+      raw: lastRaw.slice(0, 400) } };
   }
   if (data.login) {
     return { ok: false, items: [], meta: { source: 'amazon', needsAction: 'amazon-login',
diff --git a/lib/spend-reviews/drafts.js b/lib/spend-reviews/drafts.js
index 06d9341e..58c3a924 100644
--- a/lib/spend-reviews/drafts.js
+++ b/lib/spend-reviews/drafts.js
@@ -37,6 +37,19 @@ function deslop(text) {
 function hash(s) { let h = 0; for (const c of String(s)) h = (h * 31 + c.charCodeAt(0)) | 0; return h; }
 function pick(arr, seed) { return arr[Math.abs(hash(seed)) % arr.length]; }
 
+// Amazon titles are 150-200 chars of keyword soup ("Maldon - Sea Salt Flakes
+// Tub, 3.1 lb (1.4kg) - Unique Pyramid Shaped..."). A human review says "the
+// Maldon sea salt", not the whole SKU. Trim to a short, natural name.
+function shortProduct(p) {
+  if (!p) return '';
+  let s = String(p).split(/[,(|;]/)[0].trim();          // drop everything after the first , ( | ;
+  s = s.replace(/\s*[-–—]\s*/g, ' ').replace(/\s{2,}/g, ' ').trim(); // flatten internal dashes
+  const words = s.split(/\s+/);
+  if (words.length > 7) s = words.slice(0, 7).join(' ');
+  if (s.length > 48) s = s.slice(0, 48).replace(/\s+\S*$/, '').trim();
+  return s || String(p).slice(0, 40).trim();
+}
+
 // ---- sentence banks (wide, so batches don't read as clones) -------------------
 const OPEN = [
   'Really happy with this one.', 'Genuinely pleased with the whole experience.',
@@ -82,7 +95,7 @@ const RATING_LINE = [
 
 function fill(t, item) {
   const merch = item.display_merchant || item.merchant || 'them';
-  return t.replace(/\{product\}/g, item.product || 'item').replace(/\{merchant\}/g, merch);
+  return t.replace(/\{product\}/g, shortProduct(item.product) || 'item').replace(/\{merchant\}/g, merch);
 }
 
 // choose one of a few STRUCTURES per item, plus whether to append a rating line
@@ -104,7 +117,7 @@ function genGoogleReview(item) {
 
 function genAmazonReview(item) {
   const s = Math.abs(hash(item.id + 'ashape')) % 3;
-  const prod = item.product || 'this item';
+  const prod = shortProduct(item.product) || 'this item';
   const specifics = [];
   if (item.product) specifics.push(`The ${prod} is exactly as described`);
   if (item.asin) specifics.push('it arrived quickly and well packed');
@@ -122,7 +135,7 @@ function genAmazonReview(item) {
 
 function genSellerEmail(item) {
   const who = item.seller || item.display_merchant || item.merchant || 'there';
-  const thing = item.product || 'the product';
+  const thing = shortProduct(item.product) || 'the product';
   const subjBank = [`Thank you — ${thing}`, `Really pleased with the ${thing}`, `A quick thank-you for the ${thing}`, `Great experience with the ${thing}`];
   const subject = deslop(pick(subjBank, item.id + 'st'));
   const openBank = [

← cb0d3731 auto-data-snapshot: 2026-09-11T09:31:49 (1 data files) — dat  ·  back to AbramsEgo  ·  auto-data-snapshot: 2026-09-11T10:08:53 (1 data files) — dat 79d5a95a →