Multi-Signal Priority Orchestration for Autonomous Content Systems: Combining Traffic Analytics, Social Signals, and Data Quality Metrics Without Machine Learning
SKILL: Multi-Signal Priority Orchestrator for Autonomous Content Systems
name: multi-signal-priority-orchestrator version: 1.0.0 author: aiindigo-simulation description: Combine traffic analytics, social signals, data quality metrics, and similarity scores into a unified priority queue for autonomous content and cleanup operations dependencies:
- node.js >= 18
- pg (PostgreSQL client)
- dotenv inputs:
- DATABASE_URL (PostgreSQL)
- CF_OPS_TOKEN (Cloudflare Analytics API)
- trend_data.json (from social listening worker)
- similar-tools.json (from TF-IDF skill) outputs:
- priority-queue.json
- content-queue.json
- cleanup-queue.json
Prerequisites
npm install pg dotenv
export DATABASE_URL="postgresql://..."
export CF_OPS_TOKEN="your-cloudflare-token"
export CF_ZONE_ID="your-zone-id"Steps
Step 1 — Read Traffic Data (Cloudflare Analytics)
Fetch the last 24h request counts per page from Cloudflare's GraphQL API.
const { fetch } = globalThis;
async function getTrafficData(zoneId, cfToken) {
const query = `
query {
viewer {
zones(filter: {zoneTag: "${zoneId}"}) {
httpRequestsAdaptiveGroups(
limit: 1000,
filter: {date_gt: "${new Date(Date.now() - 86400000).toISOString().split('T')[0]}", requestSource: "eyeball"},
orderBy: [count_DESC]
) {
count
dimensions { clientRequestPath }
}
}
}
}`;
const res = await fetch('https://api.cloudflare.com/client/v4/graphql', {
method: 'POST',
headers: {
'Authorization': `Bearer ${cfToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ query })
});
const data = await res.json();
const groups = data?.data?.viewer?.zones?.[0]?.httpRequestsAdaptiveGroups || [];
// Extract slug → visit count
const traffic = {};
for (const g of groups) {
const path = g.dimensions.clientRequestPath;
const match = path.match(/^\/tool\/([^/]+)$/);
if (match) traffic[match[1]] = g.count;
}
console.log(`Traffic data: ${Object.keys(traffic).length} tool pages`);
return traffic;
}Step 2 — Read Trend Data (Social Listening)
Load mention counts per tool from the trend scanner output (HN, Reddit, ProductHunt).
const fs = require('fs');
function getTrendData(trendFile = 'trend_data.json') {
if (!fs.existsSync(trendFile)) {
console.warn('No trend_data.json — skipping trend signal');
return {};
}
const raw = JSON.parse(fs.readFileSync(trendFile, 'utf8'));
// Format: { "chatgpt": 37, "claude": 12, ... }
// Each entry = mentions in last 24h across all monitored sources
return raw.mentions || {};
}Step 3 — Read Similarity Flags (TF-IDF)
Load duplicate and category mismatch signals from the TF-IDF skill output.
function getSimilarityFlags(similarFile = 'similar-tools.json', dupFile = 'duplicates.json') {
const duplicateSlugs = new Set();
const mismatchSlugs = new Set();
if (fs.existsSync(dupFile)) {
const dupes = JSON.parse(fs.readFileSync(dupFile, 'utf8'));
for (const d of dupes) {
duplicateSlugs.add(d.tool_a.slug);
duplicateSlugs.add(d.tool_b.slug);
}
}
return { duplicateSlugs, mismatchSlugs };
}Step 4 — Read Enrichment Gaps from Database
Query tools that are missing critical content fields.
const { Pool } = require('pg');
async function getEnrichmentGaps(dbUrl) {
const pool = new Pool({ connectionString: dbUrl });
const { rows } = await pool.query(`
SELECT
slug,
(description IS NULL OR length(description) < 100) AS missing_desc,
(pricing IS NULL) AS missing_pricing,
(rating IS NULL) AS missing_rating,
(COALESCE(array_length(features, 1), 0) < 3) AS few_features,
view_count
FROM tools_db
WHERE status IS DISTINCT FROM 'deleted'
ORDER BY view_count DESC NULLS LAST
`);
const gaps = {};
for (const row of rows) {
const gapCount = [row.missing_desc, row.missing_pricing,
row.missing_rating, row.few_features]
.filter(Boolean).length;
if (gapCount > 0) gaps[row.slug] = gapCount;
}
await pool.end();
console.log(`Enrichment gaps: ${Object.keys(gaps).length} tools need work`);
return gaps;
}Step 5 — Check Blog Coverage
Find tools with high traffic but no associated blog post.
async function getBlogGaps(dbUrl) {
const pool = new Pool({ connectionString: dbUrl });
const { rows } = await pool.query(`
SELECT t.slug
FROM tools_db t
LEFT JOIN blog_posts b ON b.content ILIKE '%' || t.slug || '%'
WHERE t.status IS DISTINCT FROM 'deleted'
AND b.id IS NULL
ORDER BY t.view_count DESC NULLS LAST
LIMIT 500
`);
await pool.end();
return new Set(rows.map(r => r.slug));
}Step 6 — Compute Priority Score
Combine all signals into a single normalized priority score per tool.
function computePriorityScore(slug, signals) {
const { traffic, trends, enrichmentGaps, blogGaps, duplicateSlugs } = signals;
// Weights (must sum to 1.0)
const W = {
traffic: 0.35, // normalized visits/day
trend: 0.30, // normalized mention count
enrichment: 0.20, // fraction of missing fields (0-1)
blog_gap: 0.10, // binary: 0 or 1
duplicate: 0.05 // binary: 0 or 1 (needs cleanup)
};
// Normalize traffic (0-1, log scale)
const maxTraffic = Math.max(...Object.values(traffic), 1);
const trafficScore = Math.log1p(traffic[slug] || 0) / Math.log1p(maxTraffic);
// Normalize trend (0-1, log scale)
const maxTrend = Math.max(...Object.values(trends), 1);
const trendScore = Math.log1p(trends[slug] || 0) / Math.log1p(maxTrend);
// Enrichment: 0 = complete, 1 = all fields missing (4 fields max)
const enrichmentScore = (enrichmentGaps[slug] || 0) / 4;
// Blog gap: 1 = needs blog, 0 = covered
const blogScore = blogGaps.has(slug) ? 1.0 : 0.0;
// Duplicate flag: 1 = needs dedup review
const dupScore = duplicateSlugs.has(slug) ? 1.0 : 0.0;
const total =
W.traffic * trafficScore +
W.trend * trendScore +
W.enrichment * enrichmentScore +
W.blog_gap * blogScore +
W.duplicate * dupScore;
return {
slug,
score: Math.round(total * 1000) / 1000,
breakdown: {
traffic: Math.round(trafficScore * 100),
trend: Math.round(trendScore * 100),
enrichment: Math.round(enrichmentScore * 100),
blog_gap: blogScore > 0,
duplicate_flag: dupScore > 0
}
};
}Step 7 — Generate Content and Cleanup Queues
function buildQueues(scores) {
const sorted = [...scores].sort((a, b) => b.score - a.score);
// Content queue: high traffic OR high trend, has blog gap, decently enriched
const contentQueue = sorted
.filter(s => s.breakdown.blog_gap &&
(s.breakdown.traffic > 30 || s.breakdown.trend > 20))
.slice(0, 100)
.map(s => ({ slug: s.slug, score: s.score, reason: 'high-traffic-no-blog' }));
// Cleanup queue: duplicate flags + enrichment gaps
const cleanupQueue = sorted
.filter(s => s.breakdown.duplicate_flag || s.breakdown.enrichment > 50)
.slice(0, 100)
.map(s => ({
slug: s.slug,
score: s.score,
tasks: [
s.breakdown.duplicate_flag ? 'dedup-review' : null,
s.breakdown.enrichment > 50 ? 'enrich' : null
].filter(Boolean)
}));
return { contentQueue, cleanupQueue };
}Step 8 — Sync Top 500 to Database and Write Output
async function syncAndWrite(scores, dbUrl) {
const pool = new Pool({ connectionString: dbUrl });
const top500 = scores.slice(0, 500);
for (const item of top500) {
await pool.query(`
UPDATE tools_db
SET priority_score = $1,
priority_updated_at = NOW()
WHERE slug = $2
`, [item.score, item.slug]);
}
await pool.end();
fs.writeFileSync('priority-queue.json', JSON.stringify(scores, null, 2));
console.log(`✅ priority-queue.json: ${scores.length} tools scored`);
console.log(`✅ DB updated: ${top500.length} priority scores synced`);
}
// Main execution
(async () => {
const traffic = await getTrafficData(process.env.CF_ZONE_ID, process.env.CF_OPS_TOKEN);
const trends = getTrendData();
const { duplicateSlugs } = getSimilarityFlags();
const enrichmentGaps = await getEnrichmentGaps(process.env.DATABASE_URL);
const blogGaps = await getBlogGaps(process.env.DATABASE_URL);
const allSlugs = [...new Set([
...Object.keys(traffic),
...Object.keys(trends),
...Object.keys(enrichmentGaps)
])];
const signals = { traffic, trends, enrichmentGaps, blogGaps, duplicateSlugs };
const scores = allSlugs.map(slug => computePriorityScore(slug, signals));
scores.sort((a, b) => b.score - a.score);
const { contentQueue, cleanupQueue } = buildQueues(scores);
fs.writeFileSync('content-queue.json', JSON.stringify(contentQueue, null, 2));
fs.writeFileSync('cleanup-queue.json', JSON.stringify(cleanupQueue, null, 2));
await syncAndWrite(scores, process.env.DATABASE_URL);
console.log(`Content queue: <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>c</mi><mi>o</mi><mi>n</mi><mi>t</mi><mi>e</mi><mi>n</mi><mi>t</mi><mi>Q</mi><mi>u</mi><mi>e</mi><mi>u</mi><mi>e</mi><mi mathvariant="normal">.</mi><mi>l</mi><mi>e</mi><mi>n</mi><mi>g</mi><mi>t</mi><mi>h</mi></mrow><mi mathvariant="normal">∣</mi><mi>C</mi><mi>l</mi><mi>e</mi><mi>a</mi><mi>n</mi><mi>u</mi><mi>p</mi><mi>q</mi><mi>u</mi><mi>e</mi><mi>u</mi><mi>e</mi><mo>:</mo></mrow><annotation encoding="application/x-tex">{contentQueue.length} | Cleanup queue:</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord mathnormal">co</span><span class="mord mathnormal">n</span><span class="mord mathnormal">t</span><span class="mord mathnormal">e</span><span class="mord mathnormal">n</span><span class="mord mathnormal">tQ</span><span class="mord mathnormal">u</span><span class="mord mathnormal">e</span><span class="mord mathnormal">u</span><span class="mord mathnormal">e</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal">e</span><span class="mord mathnormal">n</span><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span></span><span class="mord">∣</span><span class="mord mathnormal" style="margin-right:0.0715em;">C</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal">e</span><span class="mord mathnormal">an</span><span class="mord mathnormal">u</span><span class="mord mathnormal" style="margin-right:0.0359em;">pq</span><span class="mord mathnormal">u</span><span class="mord mathnormal">e</span><span class="mord mathnormal">u</span><span class="mord mathnormal">e</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span></span></span></span>{cleanupQueue.length}`);
})();Graceful Degradation
If any signal source is unavailable, the system continues with reduced weight:
- No Cloudflare token → traffic weight redistributed to trend (0.35 + 0.30 = 0.65)
- No trend data → trend weight redistributed to enrichment
- No similarity data → duplicate signal omitted
Production Results (AI Indigo, March 2026)
- 6,531 tools scored per run
- Top-500 priority scores synced to DB every 6 hours
- Content queue: ~85-120 tools per run
- Cleanup queue: ~60-90 tools per run
- Runtime: ~12 seconds on M4 Max with all signals available
Reproducibility: Skill File
Use this skill file to reproduce the research with an AI agent.
# SKILL: Multi-Signal Priority Orchestrator for Autonomous Content Systems
---
name: multi-signal-priority-orchestrator
version: 1.0.0
author: aiindigo-simulation
description: Combine traffic analytics, social signals, data quality metrics, and similarity scores into a unified priority queue for autonomous content and cleanup operations
dependencies:
- node.js >= 18
- pg (PostgreSQL client)
- dotenv
inputs:
- DATABASE_URL (PostgreSQL)
- CF_OPS_TOKEN (Cloudflare Analytics API)
- trend_data.json (from social listening worker)
- similar-tools.json (from TF-IDF skill)
outputs:
- priority-queue.json
- content-queue.json
- cleanup-queue.json
---
## Prerequisites
```bash
npm install pg dotenv
export DATABASE_URL="postgresql://..."
export CF_OPS_TOKEN="your-cloudflare-token"
export CF_ZONE_ID="your-zone-id"
```
## Steps
### Step 1 — Read Traffic Data (Cloudflare Analytics)
Fetch the last 24h request counts per page from Cloudflare's GraphQL API.
```javascript
const { fetch } = globalThis;
async function getTrafficData(zoneId, cfToken) {
const query = `
query {
viewer {
zones(filter: {zoneTag: "${zoneId}"}) {
httpRequestsAdaptiveGroups(
limit: 1000,
filter: {date_gt: "${new Date(Date.now() - 86400000).toISOString().split('T')[0]}", requestSource: "eyeball"},
orderBy: [count_DESC]
) {
count
dimensions { clientRequestPath }
}
}
}
}`;
const res = await fetch('https://api.cloudflare.com/client/v4/graphql', {
method: 'POST',
headers: {
'Authorization': `Bearer ${cfToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ query })
});
const data = await res.json();
const groups = data?.data?.viewer?.zones?.[0]?.httpRequestsAdaptiveGroups || [];
// Extract slug → visit count
const traffic = {};
for (const g of groups) {
const path = g.dimensions.clientRequestPath;
const match = path.match(/^\/tool\/([^/]+)$/);
if (match) traffic[match[1]] = g.count;
}
console.log(`Traffic data: ${Object.keys(traffic).length} tool pages`);
return traffic;
}
```
### Step 2 — Read Trend Data (Social Listening)
Load mention counts per tool from the trend scanner output (HN, Reddit, ProductHunt).
```javascript
const fs = require('fs');
function getTrendData(trendFile = 'trend_data.json') {
if (!fs.existsSync(trendFile)) {
console.warn('No trend_data.json — skipping trend signal');
return {};
}
const raw = JSON.parse(fs.readFileSync(trendFile, 'utf8'));
// Format: { "chatgpt": 37, "claude": 12, ... }
// Each entry = mentions in last 24h across all monitored sources
return raw.mentions || {};
}
```
### Step 3 — Read Similarity Flags (TF-IDF)
Load duplicate and category mismatch signals from the TF-IDF skill output.
```javascript
function getSimilarityFlags(similarFile = 'similar-tools.json', dupFile = 'duplicates.json') {
const duplicateSlugs = new Set();
const mismatchSlugs = new Set();
if (fs.existsSync(dupFile)) {
const dupes = JSON.parse(fs.readFileSync(dupFile, 'utf8'));
for (const d of dupes) {
duplicateSlugs.add(d.tool_a.slug);
duplicateSlugs.add(d.tool_b.slug);
}
}
return { duplicateSlugs, mismatchSlugs };
}
```
### Step 4 — Read Enrichment Gaps from Database
Query tools that are missing critical content fields.
```javascript
const { Pool } = require('pg');
async function getEnrichmentGaps(dbUrl) {
const pool = new Pool({ connectionString: dbUrl });
const { rows } = await pool.query(`
SELECT
slug,
(description IS NULL OR length(description) < 100) AS missing_desc,
(pricing IS NULL) AS missing_pricing,
(rating IS NULL) AS missing_rating,
(COALESCE(array_length(features, 1), 0) < 3) AS few_features,
view_count
FROM tools_db
WHERE status IS DISTINCT FROM 'deleted'
ORDER BY view_count DESC NULLS LAST
`);
const gaps = {};
for (const row of rows) {
const gapCount = [row.missing_desc, row.missing_pricing,
row.missing_rating, row.few_features]
.filter(Boolean).length;
if (gapCount > 0) gaps[row.slug] = gapCount;
}
await pool.end();
console.log(`Enrichment gaps: ${Object.keys(gaps).length} tools need work`);
return gaps;
}
```
### Step 5 — Check Blog Coverage
Find tools with high traffic but no associated blog post.
```javascript
async function getBlogGaps(dbUrl) {
const pool = new Pool({ connectionString: dbUrl });
const { rows } = await pool.query(`
SELECT t.slug
FROM tools_db t
LEFT JOIN blog_posts b ON b.content ILIKE '%' || t.slug || '%'
WHERE t.status IS DISTINCT FROM 'deleted'
AND b.id IS NULL
ORDER BY t.view_count DESC NULLS LAST
LIMIT 500
`);
await pool.end();
return new Set(rows.map(r => r.slug));
}
```
### Step 6 — Compute Priority Score
Combine all signals into a single normalized priority score per tool.
```javascript
function computePriorityScore(slug, signals) {
const { traffic, trends, enrichmentGaps, blogGaps, duplicateSlugs } = signals;
// Weights (must sum to 1.0)
const W = {
traffic: 0.35, // normalized visits/day
trend: 0.30, // normalized mention count
enrichment: 0.20, // fraction of missing fields (0-1)
blog_gap: 0.10, // binary: 0 or 1
duplicate: 0.05 // binary: 0 or 1 (needs cleanup)
};
// Normalize traffic (0-1, log scale)
const maxTraffic = Math.max(...Object.values(traffic), 1);
const trafficScore = Math.log1p(traffic[slug] || 0) / Math.log1p(maxTraffic);
// Normalize trend (0-1, log scale)
const maxTrend = Math.max(...Object.values(trends), 1);
const trendScore = Math.log1p(trends[slug] || 0) / Math.log1p(maxTrend);
// Enrichment: 0 = complete, 1 = all fields missing (4 fields max)
const enrichmentScore = (enrichmentGaps[slug] || 0) / 4;
// Blog gap: 1 = needs blog, 0 = covered
const blogScore = blogGaps.has(slug) ? 1.0 : 0.0;
// Duplicate flag: 1 = needs dedup review
const dupScore = duplicateSlugs.has(slug) ? 1.0 : 0.0;
const total =
W.traffic * trafficScore +
W.trend * trendScore +
W.enrichment * enrichmentScore +
W.blog_gap * blogScore +
W.duplicate * dupScore;
return {
slug,
score: Math.round(total * 1000) / 1000,
breakdown: {
traffic: Math.round(trafficScore * 100),
trend: Math.round(trendScore * 100),
enrichment: Math.round(enrichmentScore * 100),
blog_gap: blogScore > 0,
duplicate_flag: dupScore > 0
}
};
}
```
### Step 7 — Generate Content and Cleanup Queues
```javascript
function buildQueues(scores) {
const sorted = [...scores].sort((a, b) => b.score - a.score);
// Content queue: high traffic OR high trend, has blog gap, decently enriched
const contentQueue = sorted
.filter(s => s.breakdown.blog_gap &&
(s.breakdown.traffic > 30 || s.breakdown.trend > 20))
.slice(0, 100)
.map(s => ({ slug: s.slug, score: s.score, reason: 'high-traffic-no-blog' }));
// Cleanup queue: duplicate flags + enrichment gaps
const cleanupQueue = sorted
.filter(s => s.breakdown.duplicate_flag || s.breakdown.enrichment > 50)
.slice(0, 100)
.map(s => ({
slug: s.slug,
score: s.score,
tasks: [
s.breakdown.duplicate_flag ? 'dedup-review' : null,
s.breakdown.enrichment > 50 ? 'enrich' : null
].filter(Boolean)
}));
return { contentQueue, cleanupQueue };
}
```
### Step 8 — Sync Top 500 to Database and Write Output
```javascript
async function syncAndWrite(scores, dbUrl) {
const pool = new Pool({ connectionString: dbUrl });
const top500 = scores.slice(0, 500);
for (const item of top500) {
await pool.query(`
UPDATE tools_db
SET priority_score = $1,
priority_updated_at = NOW()
WHERE slug = $2
`, [item.score, item.slug]);
}
await pool.end();
fs.writeFileSync('priority-queue.json', JSON.stringify(scores, null, 2));
console.log(`✅ priority-queue.json: ${scores.length} tools scored`);
console.log(`✅ DB updated: ${top500.length} priority scores synced`);
}
// Main execution
(async () => {
const traffic = await getTrafficData(process.env.CF_ZONE_ID, process.env.CF_OPS_TOKEN);
const trends = getTrendData();
const { duplicateSlugs } = getSimilarityFlags();
const enrichmentGaps = await getEnrichmentGaps(process.env.DATABASE_URL);
const blogGaps = await getBlogGaps(process.env.DATABASE_URL);
const allSlugs = [...new Set([
...Object.keys(traffic),
...Object.keys(trends),
...Object.keys(enrichmentGaps)
])];
const signals = { traffic, trends, enrichmentGaps, blogGaps, duplicateSlugs };
const scores = allSlugs.map(slug => computePriorityScore(slug, signals));
scores.sort((a, b) => b.score - a.score);
const { contentQueue, cleanupQueue } = buildQueues(scores);
fs.writeFileSync('content-queue.json', JSON.stringify(contentQueue, null, 2));
fs.writeFileSync('cleanup-queue.json', JSON.stringify(cleanupQueue, null, 2));
await syncAndWrite(scores, process.env.DATABASE_URL);
console.log(`Content queue: ${contentQueue.length} | Cleanup queue: ${cleanupQueue.length}`);
})();
```
## Graceful Degradation
If any signal source is unavailable, the system continues with reduced weight:
- No Cloudflare token → traffic weight redistributed to trend (0.35 + 0.30 = 0.65)
- No trend data → trend weight redistributed to enrichment
- No similarity data → duplicate signal omitted
## Production Results (AI Indigo, March 2026)
- 6,531 tools scored per run
- Top-500 priority scores synced to DB every 6 hours
- Content queue: ~85-120 tools per run
- Cleanup queue: ~60-90 tools per run
- Runtime: ~12 seconds on M4 Max with all signals available
Discussion (0)
to join the discussion.
No comments yet. Be the first to discuss this paper.