[object Object]

← back to Norma Platform

IG daily cadence: 1 post/day per account, 6 days/week (rest Sun) — ledger-derived coverage, auto-sourced guard-passed products per account theme, sensitive→Cody

18ad401463b1c9633ef050705d6ced03adc73c9b · 2026-08-11 13:02:46 -0700 · Steve Abrams

Files touched

Diff

commit 18ad401463b1c9633ef050705d6ced03adc73c9b
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Aug 11 13:02:46 2026 -0700

    IG daily cadence: 1 post/day per account, 6 days/week (rest Sun) — ledger-derived coverage, auto-sourced guard-passed products per account theme, sensitive→Cody
---
 agents/instagram-agent/account-themes.json |  41 ++++++++++++
 agents/instagram-agent/daily-cadence.js    | 104 +++++++++++++++++++++++++++++
 2 files changed, 145 insertions(+)

diff --git a/agents/instagram-agent/account-themes.json b/agents/instagram-agent/account-themes.json
new file mode 100644
index 0000000..9aec16e
--- /dev/null
+++ b/agents/instagram-agent/account-themes.json
@@ -0,0 +1,41 @@
+{
+  "_note": "Per-account product-search theme for the daily cadence. sensitive=true → needs Cody vision (scenic/botanical settlement risk). skip=<reason> → excluded from auto-cadence.",
+  "defaults": { "keyword_from_handle": true },
+  "overrides": {
+    "asseeninhotels":          { "keyword": "hospitality" },
+    "beverlyhillsvideos":      { "keyword": "luxury glamour" },
+    "borninbeverlyhills":      { "keyword": "glamour metallic" },
+    "brazilliancewallpaper":   { "skip": "settlement — banana-leaf/palm motif" },
+    "chinoiseriewallpaper":    { "keyword": "chinoiserie", "sensitive": true },
+    "commercialwallcoverings": { "keyword": "commercial vinyl" },
+    "customwallcoverings":     { "keyword": "mural", "sensitive": true },
+    "designerlaboratory":      { "keyword": "modern geometric" },
+    "designerwallcoverings":   { "keyword": "luxury wallcovering" },
+    "fabric_fridays":          { "keyword": "fabric" },
+    "ffepurchasing":           { "keyword": "contract commercial" },
+    "goldleafwallpaper":       { "keyword": "metal leaf gold" },
+    "grassclothwallpaper":     { "keyword": "grasscloth" },
+    "hollywoodwallcoverings":  { "keyword": "hollywood glamour" },
+    "hospitalitywallcoverings":{ "keyword": "hospitality vinyl" },
+    "interiordesignervideos":  { "keyword": "interior design" },
+    "linenwallpaper":          { "keyword": "linen" },
+    "metallicwallpaper":       { "keyword": "metallic" },
+    "papelestapiz":            { "skip": "IG account restricted (User access is restricted)" },
+    "patterndesignlab":        { "keyword": "geometric pattern" },
+    "philliperomanodesigns":   { "keyword": "phillipe romano" },
+    "restorationwallpaper":    { "keyword": "traditional damask" },
+    "retrowalls":              { "keyword": "retro mid century" },
+    "roomsettings":            { "keyword": "room setting" },
+    "screenprintedwallpaper":  { "keyword": "screen printed" },
+    "sheltermagazines":        { "keyword": "editorial interior" },
+    "suedewallpaper":          { "keyword": "suede" },
+    "textilewallpaper":        { "keyword": "textile" },
+    "thedesignerlibrary":      { "keyword": "classic damask" },
+    "thesetdecorator":         { "keyword": "dramatic scenic", "sensitive": true },
+    "traditionalwhimsy":       { "keyword": "traditional whimsical", "sensitive": true },
+    "velvetwallpaper":         { "keyword": "velvet" },
+    "wallpaperhistory":        { "keyword": "vintage damask" },
+    "wallpaperinstallers":     { "keyword": "textured wallcovering" },
+    "wallpaperwednesdays":     { "keyword": "designer wallcovering" }
+  }
+}
diff --git a/agents/instagram-agent/daily-cadence.js b/agents/instagram-agent/daily-cadence.js
new file mode 100644
index 0000000..b1248d6
--- /dev/null
+++ b/agents/instagram-agent/daily-cadence.js
@@ -0,0 +1,104 @@
+#!/usr/bin/env node
+/**
+ * daily-cadence.js — "1 post/day per account, 6 days/week" driver.
+ *
+ * Derives today's coverage from the post-ledger, picks the next batch of accounts
+ * that still need today's post, auto-sources a guard-passing (no private-label leak)
+ * on-brand product per the account's theme, and posts a PACED batch via post-to.js.
+ *
+ *   node daily-cadence.js --dry            # preview the next batch, no posting
+ *   node daily-cadence.js --batch 4        # post the next 4 accounts (default 4)
+ *   node daily-cadence.js --status         # today's coverage report only
+ *
+ * Rest day: Sundays (getDay()===0) are skipped. Sensitive (scenic/botanical) accounts
+ * are NOT auto-posted — they're reported as "needs Cody" for a gated visual pass.
+ * Restricted / settlement-skip accounts are excluded entirely.
+ */
+const fs = require('fs');
+const path = require('path');
+const cp = require('child_process');
+
+const HERE = __dirname;
+const args = process.argv.slice(2);
+const flag = (n) => args.includes('--' + n);
+const val = (n, d) => { const i = args.indexOf('--' + n); return i >= 0 ? args[i + 1] : d; };
+const DRY = flag('dry');
+const BATCH = parseInt(val('batch', '4'), 10);
+const STORE = 'https://designerwallcoverings.com';
+
+const reg = JSON.parse(fs.readFileSync(path.join(HERE, 'accounts.json'), 'utf8')).accounts;
+const themes = JSON.parse(fs.readFileSync(path.join(HERE, 'account-themes.json'), 'utf8')).overrides;
+const content = require('./content');
+
+const HANDLES = Object.keys(reg);
+const now = new Date();
+const isRestDay = now.getDay() === 0; // Sunday
+const today = now.toISOString().slice(0, 10);
+
+// posted today (from ledger)
+const led = path.join(HERE, 'data', 'post-ledger.jsonl');
+const rows = fs.existsSync(led) ? fs.readFileSync(led, 'utf8').split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean) : [];
+const postedToday = new Set(rows.filter((r) => (r.ts || '').slice(0, 10) === today).map((r) => r.handle));
+const everPosted = new Set(rows.map((r) => r.product_handle)); // avoid reusing a product
+
+function themeFor(h) {
+  const o = themes[h] || {};
+  if (o.skip) return { skip: o.skip };
+  const keyword = o.keyword || h.replace(/wallpaper|wallcoverings|walls|designs?/gi, '').replace(/[_-]+/g, ' ').trim() || 'wallcovering';
+  return { keyword, sensitive: !!o.sensitive };
+}
+
+async function findProduct(keyword) {
+  const u = `${STORE}/search/suggest.json?q=${encodeURIComponent(keyword)}&resources%5Btype%5D=product&resources%5Blimit%5D=10`;
+  let list = [];
+  try { const j = await (await fetch(u)).json(); list = (j.resources?.results?.products) || []; } catch { return null; }
+  for (const p of list) {
+    if (everPosted.has(p.handle)) continue; // don't repost a product
+    try {
+      const r = await content.resolveProduct(p.handle); // throws on leak/no-image (guard)
+      return { handle: p.handle, title: r.title, image_url: r.image_url };
+    } catch { /* leak or imageless — next */ }
+  }
+  return null;
+}
+
+(async () => {
+  if (isRestDay) { console.log('REST DAY (Sunday) — no posting today.'); return; }
+
+  const eligible = HANDLES.filter((h) => !themeFor(h).skip);
+  const remaining = eligible.filter((h) => !postedToday.has(h));
+  const skipped = HANDLES.filter((h) => themeFor(h).skip).map((h) => `${h} (${themeFor(h).skip})`);
+
+  if (flag('status')) {
+    console.log(`COVERAGE ${today}: ${postedToday.size}/${eligible.length} eligible accounts posted; ${remaining.length} remaining.`);
+    console.log(`Excluded: ${skipped.join(', ') || 'none'}`);
+    console.log(`Remaining: ${remaining.join(', ') || '(day complete)'}`);
+    return;
+  }
+
+  if (!remaining.length) { console.log(`DAY COMPLETE — all ${eligible.length} eligible accounts posted for ${today}.`); return; }
+
+  // next batch, sensitive accounts deferred to Cody (reported, not auto-posted)
+  const batch = [];
+  const needsCody = [];
+  for (const h of remaining) {
+    if (batch.length >= BATCH) break;
+    const t = themeFor(h);
+    if (t.sensitive) { needsCody.push(h); continue; }
+    const prod = await findProduct(t.keyword);
+    if (!prod) { console.log(`  ⚠ @${h} — no clean product for "${t.keyword}" (all leaked/reused); will retry`); continue; }
+    batch.push({ account: h, product: prod.handle, title: prod.title });
+  }
+
+  console.log(`Daily cadence ${today} — ${postedToday.size}/${eligible.length} done, posting next ${batch.length}${DRY ? ' (DRY)' : ''}:`);
+  for (const b of batch) {
+    console.log(`  @${b.account} ← ${b.title}  [${b.product}]`);
+    if (!DRY) {
+      try {
+        cp.execFileSync(process.execPath, [path.join(HERE, 'post-to.js'), b.account, '--product', b.product, '--confirm'], { stdio: 'ignore' });
+      } catch (e) { console.log(`    ✗ post failed: ${e.message}`); }
+    }
+  }
+  if (needsCody.length) console.log(`Deferred to Cody (settlement-sensitive, gated visual pass): ${needsCody.join(', ')}`);
+  console.log(`Remaining after this batch: ${remaining.length - batch.length}`);
+})();

← f0e553d IG captions: strip internal mfr SKU numbers from title (Koro  ·  back to Norma Platform  ·  daily-cadence: velocity tripwire (stop batch on 'restricted' 1ec2435 →