← back to Marketing Command Center
meta-token-health: retry transient Graph blips + only prescribe token-paste on true expiry (fix 8/20 false alarm)
8718269fecc1a91ac291d22974579158c92b2449 · 2026-08-20 09:21:02 -0700 · Steve Abrams
Files touched
M scripts/meta-token-health.js
Diff
commit 8718269fecc1a91ac291d22974579158c92b2449
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Aug 20 09:21:02 2026 -0700
meta-token-health: retry transient Graph blips + only prescribe token-paste on true expiry (fix 8/20 false alarm)
---
scripts/meta-token-health.js | 61 ++++++++++++++++++++++++++++++++++++--------
1 file changed, 51 insertions(+), 10 deletions(-)
diff --git a/scripts/meta-token-health.js b/scripts/meta-token-health.js
index 041093a..3c8ed66 100644
--- a/scripts/meta-token-health.js
+++ b/scripts/meta-token-health.js
@@ -36,21 +36,62 @@ async function alert(subject, body) {
}
}
+// A genuine, act-on-it expiry (token is really dead — pasting a new one is the fix).
+// Meta expiry surfaces as subcode 463/467/460 or an unambiguous message; a bare
+// code 190 with a "cannot parse"/"malformed" message is a TRANSIENT Graph blip,
+// NOT an expiry (the 2026-08-20 false alarm: same static token failed at 08:05,
+// validated at 09:19, .env untouched). Don't prescribe a token-paste for those.
+const isTrueExpiry = (err) => {
+ const sub = err.error_subcode;
+ if (sub === 463 || sub === 467 || sub === 460) return true;
+ return /expired|session has expired|been invalidated|revoked|changed your password|re-?authenticate/i.test(err.message || '');
+};
+// Retry-worthy: transient blips (parse/malformed/rate/5xx, or code 190 that is NOT a true expiry).
+const isTransient = (err, httpStatus) => {
+ const m = err.message || '';
+ if (/cannot parse|malformed|temporarily|try again|unexpected error|please reduce|rate limit|an error occurred/i.test(m)) return true;
+ if ([1, 2, 4, 17, 341, 368].includes(err.code)) return true; // Meta transient/rate classes
+ if (httpStatus >= 500) return true;
+ if (err.code === 190 && !isTrueExpiry(err)) return true; // bare 190 w/o expiry signal → treat as blip
+ return false;
+};
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+
(async () => {
const stamp = (status, detail) => { try { fs.writeFileSync(STAMP, JSON.stringify({ ok: status === 'ok', status, detail, checkedAt: new Date().toISOString() }, null, 2)); } catch {} };
if (!TOKEN) { console.log('DEAD: no META_ACCESS_TOKEN in .env'); stamp('missing', 'no token in .env'); await alert('NOT SET', 'No META_ACCESS_TOKEN in MCC .env — Vendor IG panel cannot fetch posts.'); process.exit(0); }
- let r, j;
- try { r = await fetch(`${GRAPH}/me?fields=id,name&access_token=${encodeURIComponent(TOKEN)}`); j = await r.json(); }
- catch (e) { console.log('NET ERR', e.message); stamp('neterr', e.message); process.exit(0); }
+ // Check /me with up to 3 attempts. Break early on success or a TRUE expiry
+ // (retrying a genuinely dead token is pointless); keep retrying transient blips
+ // so a single Graph hiccup no longer pages Steve with a wrong "paste a token".
+ let j = null, httpStatus = 0, lastErr = null;
+ for (let attempt = 1; attempt <= 3; attempt++) {
+ try {
+ const r = await fetch(`${GRAPH}/me?fields=id,name&access_token=${encodeURIComponent(TOKEN)}`);
+ httpStatus = r.status; j = await r.json();
+ } catch (e) { lastErr = { message: e.message, network: true }; j = null; }
- if (j.error) {
- const m = j.error.message || '';
- const expired = j.error.code === 190 || /expired|session|validating access token/i.test(m);
- console.log('DEAD:', m);
- stamp('expired', m);
- await alert(expired ? 'EXPIRED / INVALID' : 'ERROR',
- `${m}\n\nThe Vendor IG panel (last-10 posts) is dark until you paste a fresh long-lived META_ACCESS_TOKEN (scopes: instagram_basic, pages_read_engagement, business_management) → route via the secrets skill into MCC .env, then click Refresh on the panel.`);
+ if (j && !j.error) { lastErr = null; break; } // success
+ lastErr = j?.error || lastErr || { message: 'unknown error' };
+ if (j?.error && isTrueExpiry(j.error)) break; // truly dead — stop retrying
+ if (attempt < 3) { console.log(`retry ${attempt}: ${lastErr.message}`); await sleep(1500); }
+ }
+
+ if (lastErr) {
+ const m = lastErr.message || '';
+ if (!lastErr.network && lastErr.code !== undefined && isTrueExpiry(lastErr)) {
+ // Genuine expiry — the token really is dead; pasting a fresh one is correct.
+ console.log('DEAD (expired):', m); stamp('expired', m);
+ await alert('EXPIRED / INVALID',
+ `${m}\n\nThe Vendor IG panel (last-10 posts) is dark until you paste a fresh long-lived META_ACCESS_TOKEN (scopes: instagram_basic, pages_read_engagement, business_management) → route via the secrets skill into MCC .env, then click Refresh on the panel.`);
+ } else {
+ // Persisted across retries but NOT an expiry signal → transient Graph error
+ // or a consumer problem. Do NOT tell Steve to paste a token (the secret is
+ // likely fine). Point at the real check instead.
+ console.log('TRANSIENT/UNCLEAR:', m); stamp('transient', m);
+ await alert('TRANSIENT ERROR (token likely OK)',
+ `Graph returned a non-expiry error 3x: "${m}"\n\nThis is NOT a token expiry — do NOT paste a new token yet. Likely a Meta Graph blip or the MCC consumer (:9661) not running. Verify: re-run scripts/meta-token-health.js, and if it says LIVE the token is fine; if the panel is still dark, restart the MCC server rather than rotating the token.`);
+ }
process.exit(0);
}
← 62c9b25 auto-data-snapshot: 2026-08-20T09:06:02 (1 data files) — pub
·
back to Marketing Command Center
·
auto-data-snapshot: 2026-08-20T09:38:44 (2 data files) — dat 169f8b1 →