← back to Dw Yolo Loop
GMC $4.25 feed-exclusion: staged Shopify-side fix (dry-run: 60,232 mispriced products in Google feed)
0bd224f311367809059a4630c72ff47345e88d61 · 2026-06-15 22:50:37 -0700 · Steve Abrams
Unpublishes Bucket A+B (min-variant=$4.25) from the Google & YouTube channel
(pub 29646651457) only — Steve-controllable, no Merchant Center login needed.
Dry-run default (read-only); --apply gated behind --yes-i-am-steve.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
Diff
commit 0bd224f311367809059a4630c72ff47345e88d61
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jun 15 22:50:37 2026 -0700
GMC $4.25 feed-exclusion: staged Shopify-side fix (dry-run: 60,232 mispriced products in Google feed)
Unpublishes Bucket A+B (min-variant=$4.25) from the Google & YouTube channel
(pub 29646651457) only — Steve-controllable, no Merchant Center login needed.
Dry-run default (read-only); --apply gated behind --yes-i-am-steve.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
gmc-feed-exclude.js | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 166 insertions(+)
diff --git a/gmc-feed-exclude.js b/gmc-feed-exclude.js
new file mode 100644
index 0000000..9975202
--- /dev/null
+++ b/gmc-feed-exclude.js
@@ -0,0 +1,166 @@
+#!/usr/bin/env node
+/**
+ * GMC $4.25 feed-exclusion — STAGED FIX (Shopify-side, Steve-controllable).
+ *
+ * Root cause: Google Shopping feeds the MIN variant price; on DW the $4.25
+ * "-Sample" variant is almost always the min, so ~62k active products would
+ * advertise at $4.25. GMC's auto-add (June 18) pulls from products published
+ * to the Shopify "Google & YouTube" channel (publication 29646651457).
+ *
+ * Surgical fix: UNPUBLISH the mispriced products (Bucket A+B: min-variant price
+ * == $4.25) from the Google & YouTube channel ONLY. They leave the feed → GMC
+ * has nothing to auto-add. Online Store + all other channels are untouched.
+ * Does NOT require the wallsandfabrics@gmail.com Merchant Center login — Steve
+ * fires this from the Shopify admin he already controls.
+ *
+ * node gmc-feed-exclude.js # DRY-RUN (default): read-only scan,
+ * # counts + writes worklist JSON. No writes to Shopify.
+ * node gmc-feed-exclude.js --apply # GATED: actually unpublishes from Google channel.
+ * # Requires --yes-i-am-steve to run. DO NOT run autonomously.
+ *
+ * Cost: $0 (Shopify Admin API, no per-call charge; no LLM/paid API).
+ */
+const fs = require('fs');
+
+const SHOP = 'designer-laboratory-sandbox.myshopify.com';
+const VERSION = '2024-10';
+const ENV = fs.readFileSync('/Users/stevestudio2/Projects/secrets-manager/.env', 'utf8');
+const TOKEN = (ENV.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1];
+if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+
+const URL = `https://${SHOP}/admin/api/${VERSION}/graphql.json`;
+const SAMPLE_PRICE = 4.25;
+const GOOGLE_PUBLICATION_ID = 'gid://shopify/Publication/29646651457'; // "Google & YouTube"
+
+const args = process.argv.slice(2);
+const APPLY = args.includes('--apply');
+const CONFIRMED = args.includes('--yes-i-am-steve');
+const WORKLIST = '/Users/stevestudio2/.claude/yolo-queue/gmc-feed-exclude-worklist.json';
+
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+
+async function gql(query, variables) {
+ for (let attempt = 0; attempt < 7; attempt++) {
+ let res;
+ try {
+ res = await fetch(URL, {
+ method: 'POST',
+ headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+ body: JSON.stringify({ query, variables }),
+ });
+ } catch (e) { await sleep(2000 * (attempt + 1)); continue; }
+ if (res.status === 429) { await sleep(2000 * (attempt + 1)); continue; }
+ const json = await res.json();
+ if (json.errors) {
+ if (JSON.stringify(json.errors).includes('THROTTLED')) { await sleep(2000 * (attempt + 1)); continue; }
+ throw new Error(JSON.stringify(json.errors));
+ }
+ const avail = json.extensions?.cost?.throttleStatus?.currentlyAvailable ?? 4000;
+ if (avail < 400) await sleep(1500);
+ return json.data;
+ }
+ throw new Error('exhausted retries');
+}
+
+// Scan query: id, vendor, variant prices, and whether currently on the Google channel.
+const QUERY = `
+query($cursor: String) {
+ products(first: 100, after: $cursor, query: "status:active") {
+ pageInfo { hasNextPage endCursor }
+ nodes {
+ id
+ title
+ vendor
+ onGoogle: publishedOnPublication(publicationId: "${GOOGLE_PUBLICATION_ID}")
+ variants(first: 60) { nodes { title price } }
+ }
+ }
+}`;
+
+function minVariantPrice(variants) {
+ let min = Infinity;
+ for (const v of variants) { const p = parseFloat(v.price); if (!isNaN(p)) min = Math.min(min, p); }
+ return min;
+}
+function hasRealRoll(variants) {
+ // a non-sample, priced variant above the sample price exists
+ return variants.some(v => parseFloat(v.price) > SAMPLE_PRICE);
+}
+
+async function scan() {
+ let cursor = null, total = 0, pages = 0;
+ const targets = []; // Bucket A+B products (min-variant == 4.25) currently ON the Google channel
+ let aPlusB = 0, onGoogleAB = 0, notOnGoogleAB = 0;
+ const t0 = Date.now();
+ while (true) {
+ const data = await gql(QUERY, { cursor });
+ const conn = data.products;
+ for (const n of conn.nodes) {
+ total++;
+ const vs = n.variants.nodes;
+ const min = minVariantPrice(vs);
+ const feeds425 = Math.abs(min - SAMPLE_PRICE) < 0.005 || min < SAMPLE_PRICE + 0.005;
+ if (feeds425) {
+ aPlusB++;
+ if (n.onGoogle) { onGoogleAB++; targets.push({ id: n.id, title: n.title, vendor: n.vendor, min, bucket: hasRealRoll(vs) ? 'B' : 'A' }); }
+ else notOnGoogleAB++;
+ }
+ }
+ pages++;
+ if (pages % 50 === 0) process.stderr.write(` ...${pages} pages, ${total} products, AB=${aPlusB} (onGoogle=${onGoogleAB})\n`);
+ if (!conn.pageInfo.hasNextPage) break;
+ cursor = conn.pageInfo.endCursor;
+ }
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
+ return { total, pages, elapsed, aPlusB, onGoogleAB, notOnGoogleAB, targets };
+}
+
+async function unpublish(ids) {
+ // publishableUnpublish from the Google & YouTube channel only.
+ let done = 0, failed = 0;
+ for (const id of ids) {
+ try {
+ const r = await gql(
+ `mutation($id:ID!,$pubs:[PublicationInput!]!){ publishableUnpublish(id:$id, input:$pubs){ userErrors{ message } } }`,
+ { id, pubs: [{ publicationId: GOOGLE_PUBLICATION_ID }] }
+ );
+ const ue = r.publishableUnpublish?.userErrors;
+ if (ue && ue.length) { failed++; if (failed <= 5) console.error(' ERR', id, ue[0].message); }
+ else done++;
+ } catch (e) { failed++; if (failed <= 5) console.error(' ERR', id, e.message); }
+ if (done % 200 === 0 && done) console.error(` unpublished ${done}...`);
+ }
+ return { done, failed };
+}
+
+(async () => {
+ console.log(`GMC feed-exclusion — ${APPLY ? 'APPLY' : 'DRY-RUN'} mode`);
+ console.log(`Target: unpublish active products with min-variant price <= $${SAMPLE_PRICE} from "Google & YouTube" (pub 29646651457)\n`);
+ const r = await scan();
+ console.log(`\nScanned ${r.total} active products in ${r.elapsed}s (${r.pages} pages).`);
+ console.log(`Bucket A+B (would feed $4.25): ${r.aPlusB}`);
+ console.log(` • currently ON Google channel (in the feed → WOULD be excluded): ${r.onGoogleAB}`);
+ console.log(` • NOT on Google channel (already absent from feed): ${r.notOnGoogleAB}`);
+ fs.writeFileSync(WORKLIST, JSON.stringify({
+ generated_at: new Date().toISOString(),
+ google_publication_id: GOOGLE_PUBLICATION_ID,
+ scanned_active: r.total,
+ bucket_a_plus_b: r.aPlusB,
+ on_google_would_exclude: r.onGoogleAB,
+ not_on_google: r.notOnGoogleAB,
+ targets: r.targets,
+ }, null, 2));
+ console.log(`\nWorklist written: ${WORKLIST} (${r.targets.length} product IDs)`);
+
+ if (!APPLY) {
+ console.log(`\nDRY-RUN only — zero writes to Shopify. To apply (GATED): node gmc-feed-exclude.js --apply --yes-i-am-steve`);
+ return;
+ }
+ if (!CONFIRMED) {
+ console.error(`\nREFUSING to apply without --yes-i-am-steve. This unpublishes ${r.onGoogleAB} products from the Google channel. Aborting.`);
+ process.exit(2);
+ }
+ console.log(`\nAPPLYING: unpublishing ${r.targets.length} products from Google & YouTube...`);
+ const res = await unpublish(r.targets.map(t => t.id));
+ console.log(`Done: ${res.done} unpublished, ${res.failed} failed.`);
+})().catch(e => { console.error('FATAL', e.message); process.exit(1); });
← 6e2c6a1 Stage banned-word Wallpaper->Wallcovering rewrite PLAN + AI-
·
back to Dw Yolo Loop
·
Stage Kravet below-MAP fix-list (208 ROLL-eligible, 2026-06- 21bdbc0 →