← back to Norma
IG reshare: blocklist Spoonflower (competitor) fleet-wide + scrub vendor file + add scan tool
b5cd441d94b4d25ce54ff911a7a5c4f22bbdfef4 · 2026-08-17 07:23:26 -0700 · Steve Abrams
Spoonflower is a print-on-demand competitor, not a DW vendor, but it was in
dw-vendors.txt (and vendor_registry), so the reshare engine credited it on
grassclothwallpaper/suedewallpaper. Added a code-level BLOCKLIST guard in
attribute() in both hashtag-reshare.js and fabric-friday-reshare.js so a
competitor is never credited/reshared even if it re-enters the vendor file.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M agents/instagram-agent/data/dw-vendors.txtM agents/instagram-agent/fabric-friday-reshare.jsM agents/instagram-agent/hashtag-reshare.jsA agents/instagram-agent/scan-spoonflower.js
Diff
commit b5cd441d94b4d25ce54ff911a7a5c4f22bbdfef4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 17 07:23:26 2026 -0700
IG reshare: blocklist Spoonflower (competitor) fleet-wide + scrub vendor file + add scan tool
Spoonflower is a print-on-demand competitor, not a DW vendor, but it was in
dw-vendors.txt (and vendor_registry), so the reshare engine credited it on
grassclothwallpaper/suedewallpaper. Added a code-level BLOCKLIST guard in
attribute() in both hashtag-reshare.js and fabric-friday-reshare.js so a
competitor is never credited/reshared even if it re-enters the vendor file.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
agents/instagram-agent/data/dw-vendors.txt | 1 -
agents/instagram-agent/fabric-friday-reshare.js | 12 +++++
agents/instagram-agent/hashtag-reshare.js | 11 ++++
agents/instagram-agent/scan-spoonflower.js | 67 +++++++++++++++++++++++++
4 files changed, 90 insertions(+), 1 deletion(-)
diff --git a/agents/instagram-agent/data/dw-vendors.txt b/agents/instagram-agent/data/dw-vendors.txt
index 2036bee..fd8c44c 100644
--- a/agents/instagram-agent/data/dw-vendors.txt
+++ b/agents/instagram-agent/data/dw-vendors.txt
@@ -400,7 +400,6 @@ sidney paul
sister parish
source one wallcovering
spinneybeck leather
-spoonflower
stacy garcia commercial wallpaper
stark
stout textiles
diff --git a/agents/instagram-agent/fabric-friday-reshare.js b/agents/instagram-agent/fabric-friday-reshare.js
index b116520..20ca89a 100644
--- a/agents/instagram-agent/fabric-friday-reshare.js
+++ b/agents/instagram-agent/fabric-friday-reshare.js
@@ -29,6 +29,15 @@ const VENDORS = fs.readFileSync(path.join(__dirname, 'data', 'dw-vendors.txt'),
.split('\n').map((s) => s.trim().toLowerCase()).filter((s) => s.length >= 5)
.sort((a, b) => b.length - a.length);
+// Competitor blocklist — NEVER reshare or credit these on Steve's accounts, even if one
+// slips into dw-vendors.txt (Spoonflower is a print-on-demand COMPETITOR, not a DW vendor).
+// Steve, 2026-08-17. Add competitors here to block them fleet-wide across both reshare tools.
+const BLOCKLIST = ['spoonflower'];
+const isBlocked = (s) => {
+ const a = String(s || '').toLowerCase().replace(/[^a-z0-9]/g, '');
+ return BLOCKLIST.some((b) => a.includes(b.replace(/[^a-z0-9]/g, '')));
+};
+
// Tasteful, complimentary sass — hypes the maker, never mocks. Varied so reshares aren't identical.
const SASS = [
(u) => `Okay ${u}, this is doing WAY too much — in the exact right way. 🧵 That's how you Fabric Friday. 👏`,
@@ -44,6 +53,9 @@ const titleCase = (s) => s.replace(/\b([a-z])/g, (m) => m.toUpperCase());
function attribute(caption) {
const cap = caption || '';
const mentions = [...new Set((cap.match(/@[a-zA-Z0-9_.]{2,}/g) || []))];
+ // Blocklist gate: if a competitor appears anywhere in the caption or a mention, refuse to
+ // attribute it → the reshare loop skips it (no creditName). Never promote a competitor.
+ if (isBlocked(cap) || mentions.some(isBlocked)) return { mentions: [], vendor: null, creditName: null };
const capL = cap.toLowerCase();
const capAlnum = capL.replace(/[^a-z0-9]/g, ''); // "by brunschwigfils" -> "bybrunschwigfils"
// Match vendors both loosely AND normalized (so "brunschwig & fils" aligns to "brunschwigfils"),
diff --git a/agents/instagram-agent/hashtag-reshare.js b/agents/instagram-agent/hashtag-reshare.js
index 8b3dc01..b714991 100644
--- a/agents/instagram-agent/hashtag-reshare.js
+++ b/agents/instagram-agent/hashtag-reshare.js
@@ -34,6 +34,15 @@ if (!TAGS.length) { console.error(`No hashtags mapped for @${ACCOUNT} (skip/rest
const VENDORS = fs.readFileSync(path.join(__dirname, 'data', 'dw-vendors.txt'), 'utf8')
.split('\n').map((s) => s.trim().toLowerCase()).filter((s) => s.length >= 5).sort((a, b) => b.length - a.length);
+// Competitor blocklist — NEVER reshare or credit these on Steve's accounts, even if one
+// slips into dw-vendors.txt (Spoonflower is a print-on-demand COMPETITOR, not a DW vendor).
+// Steve, 2026-08-17. Add competitors here to block them fleet-wide across both reshare tools.
+const BLOCKLIST = ['spoonflower'];
+const isBlocked = (s) => {
+ const a = String(s || '').toLowerCase().replace(/[^a-z0-9]/g, '');
+ return BLOCKLIST.some((b) => a.includes(b.replace(/[^a-z0-9]/g, '')));
+};
+
const SASS = [
(u) => `Okay ${u}, this is doing WAY too much — in the exact right way. 👏`,
(u) => `We saw ${u} pull up with THIS and had to share. The audacity of good taste. ✨`,
@@ -62,6 +71,8 @@ async function hashtagIds() {
function attribute(caption) {
const cap = caption || '';
const mentions = [...new Set((cap.match(/@[a-zA-Z0-9_.]{2,}/g) || []))];
+ // Blocklist gate: refuse to attribute a competitor → reshare loop skips it (no creditName).
+ if (isBlocked(cap) || mentions.some(isBlocked)) return { mentions: [], vendor: null, creditName: null };
const capL = cap.toLowerCase(); const capAlnum = capL.replace(/[^a-z0-9]/g, '');
const hits = [];
for (const v of VENDORS) { const vn = v.replace(/[^a-z0-9]/g, ''); if (capL.includes(v) || (vn.length >= 6 && capAlnum.includes(vn))) hits.push(v); }
diff --git a/agents/instagram-agent/scan-spoonflower.js b/agents/instagram-agent/scan-spoonflower.js
new file mode 100644
index 0000000..2371304
--- /dev/null
+++ b/agents/instagram-agent/scan-spoonflower.js
@@ -0,0 +1,67 @@
+#!/usr/bin/env node
+/**
+ * scan-spoonflower.js — READ-ONLY fleet-wide scan for any post whose caption mentions
+ * "spoonflower" across all 35 IG accounts. Uses the shared META token + each account's
+ * ig_user_id. Paginates up to MAXPAGES per account. Writes data/spoonflower-hitlist.jsonl.
+ * No writes to Instagram. $0 (Graph API reads are free).
+ */
+const fs = require('fs');
+const path = require('path');
+const https = require('https');
+const A = require('./accounts');
+
+const HOST = 'graph.facebook.com';
+const VER = 'v21.0';
+const MAXPAGES = 6; // ~6*50 = up to 300 recent posts/account
+const RE = /spoonflower/i;
+const OUT = path.join(__dirname, 'data', 'spoonflower-hitlist.jsonl');
+
+function get(url) {
+ return new Promise((resolve, reject) => {
+ https.get(url, (res) => {
+ let d = '';
+ res.on('data', (c) => (d += c));
+ res.on('end', () => { try { resolve(JSON.parse(d)); } catch (e) { reject(new Error('bad json: ' + d.slice(0, 200))); } });
+ }).on('error', reject);
+ });
+}
+
+async function scanAccount(acct) {
+ const hits = [];
+ let url = `https://${HOST}/${VER}/${acct.ig_user_id}/media?fields=id,caption,permalink,timestamp,media_type&limit=50&access_token=${encodeURIComponent(acct.access_token)}`;
+ let scanned = 0;
+ for (let page = 0; page < MAXPAGES && url; page++) {
+ const j = await get(url);
+ if (j.error) return { error: j.error.message, scanned, hits };
+ for (const m of (j.data || [])) {
+ scanned++;
+ if (m.caption && RE.test(m.caption)) {
+ hits.push({ handle: acct.handle, page_name: acct.page_name, ig_user_id: acct.ig_user_id,
+ media_id: m.id, permalink: m.permalink, timestamp: m.timestamp, media_type: m.media_type,
+ caption: m.caption });
+ }
+ }
+ url = (j.paging && j.paging.next) || null;
+ }
+ return { scanned, hits };
+}
+
+(async () => {
+ const accounts = A.list().map((a) => A.resolve(a.handle)).filter((a) => a && a.ig_user_id && a.access_token);
+ console.log(`Scanning ${accounts.length} accounts (up to ${MAXPAGES * 50} posts each)…\n`);
+ const all = [];
+ const errs = [];
+ for (const acct of accounts) {
+ try {
+ const r = await scanAccount(acct);
+ if (r.error) { errs.push({ handle: acct.handle, error: r.error }); process.stdout.write(` @${acct.handle}: ERR ${r.error.slice(0,60)}\n`); continue; }
+ if (r.hits.length) { all.push(...r.hits); console.log(` @${acct.handle}: ${r.hits.length} SPOONFLOWER hit(s) / ${r.scanned} scanned`); }
+ else process.stdout.write(` @${acct.handle}: clean (${r.scanned})\n`);
+ } catch (e) { errs.push({ handle: acct.handle, error: e.message }); console.log(` @${acct.handle}: EXC ${e.message.slice(0,60)}`); }
+ }
+ fs.writeFileSync(OUT, all.map((h) => JSON.stringify(h)).join('\n') + (all.length ? '\n' : ''));
+ console.log(`\n=== TOTAL: ${all.length} Spoonflower posts across ${new Set(all.map(h=>h.handle)).size} accounts ===`);
+ console.log(`hitlist → ${OUT}`);
+ if (errs.length) console.log(`errors on ${errs.length} accounts:`, errs.map(e=>e.handle).join(', '));
+ for (const h of all) console.log(` • @${h.handle} ${h.permalink} (${h.timestamp}) "${(h.caption||'').replace(/\n/g,' ').slice(0,70)}…"`);
+})();
← ec91871 Gate live IG delete behind IG_LIVE_DELETE flag (default off,
·
back to Norma
·
ig: wrong-image delete tooling — 4 posts (metallic/suede), p 1b1e7d4 →