Cross-list narrative resonance + signal-scored top tweets from tracked X lists in the past 24 hours
Scanned 6/6/2026
Install to Claude Code
npx -y skills add anajuliabit/aeon --skill list-digest --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of List Digest?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anajuliabit-list-digest)More formats (shields.io, HTML) on the badges page.
---
name: List Digest
description: Cross-list narrative resonance + signal-scored top tweets from tracked X lists in the past 24 hours
var: ""
tags: [social]
---
<!-- autoresearch: variation B — sharper output via signal scoring + cross-list narrative clustering + insight-per-item discipline -->
> **${var}** — Comma-separated X list IDs to track (e.g. `"1953536336675365173,1937207796270829766"`). Optionally append a topic filter after a pipe: `"LIST_ID1,LIST_ID2|AI agents"`. List IDs are the numeric IDs in the URL (`https://x.com/i/lists/<ID>`). **Required** — set in `aeon.yml`.
Read `memory/MEMORY.md` for context. Read the last 2 days of `memory/logs/` and `memory/list-digest-seen.txt` (if it exists) to deduplicate.
## Why this skill exists
Tracked X lists are *curator signal* — someone you trust pre-selected the accounts. The value isn't a flat top-N-per-list dump (that buries the lede), it's:
1. **Cross-list resonance** — when ≥2 lists surface the same story, that's stronger than any single-list winner.
2. **Insight, not paraphrase** — each item must answer "so what" in one line, not restate the tweet.
3. **A verdict** — one line at the top telling the operator what today's lists collectively say.
## Steps
### 1. Parse and validate `${var}`
```bash
if [ -z "${var}" ]; then
echo "LIST_DIGEST_NO_CONFIG: var must contain at least one X list ID" \
>> "memory/logs/$(date -u +%Y-%m-%d).md"
exit 0
fi
IDS_PART="${var%%|*}"
TOPIC_FILTER=""
if [ "${var}" != "$IDS_PART" ]; then
TOPIC_FILTER="${var#*|}"
fi
# Validate: each ID must be all digits (X list IDs are numeric)
for LIST_ID in $(echo "$IDS_PART" | tr ',' ' '); do
if ! [[ "$LIST_ID" =~ ^[0-9]+$ ]]; then
echo "LIST_DIGEST_NO_CONFIG: invalid list ID '$LIST_ID' (must be numeric)" \
>> "memory/logs/$(date -u +%Y-%m-%d).md"
exit 0
fi
done
```
If `XAI_API_KEY` is unset and no cache exists, fall back to **Path C** in step 2. If no path returns data, log `LIST_DIGEST_NO_CONFIG: XAI_API_KEY required` and stop without notifying.
### 2. Fetch each list's top tweets (past 24h)
For each `LIST_ID`, prefer cache → API → WebSearch in that order.
**Path A — pre-fetched cache** (preferred — `scripts/prefetch-xai.sh` may have run):
```bash
cat ".xai-cache/list-digest-${LIST_ID}.json" 2>/dev/null \
| jq -r '.output[] | select(.type == "message") | .content[] | select(.type == "output_text") | .text'
```
**Path B — X.AI Responses API**:
```bash
FROM_DATE=$(date -u -d "yesterday" +%Y-%m-%d 2>/dev/null || date -u -v-1d +%Y-%m-%d)
TO_DATE=$(date -u +%Y-%m-%d)
curl -s --max-time 180 -X POST "https://api.x.ai/v1/responses" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $XAI_API_KEY" \
-d '{
"model": "grok-4-1-fast",
"input": [{"role": "user", "content": "Look at X list https://x.com/i/lists/'"$LIST_ID"'. Step 1: report the list name and a one-line description. Step 2: identify the most engaging tweets posted by members of this list between '"$FROM_DATE"' and '"$TO_DATE"' UTC. Return the top 12 tweets ranked by engagement (likes, retweets, replies). For EACH tweet you MUST return: (a) @handle, (b) the full tweet text (not a paraphrase), (c) explicit engagement counts as separate fields — likes:N, retweets:N, replies:N, views:N if available, (d) the direct permalink in the form https://x.com/<handle>/status/<id>, (e) media type (image|video|none), (f) one-line context if it'\''s a reply or quote tweet (who/what). Skip retweets of accounts NOT on this list. If a tweet has an image and you can analyze it, include a one-line image description."}],
"tools": [{"type": "x_search", "from_date": "'"$FROM_DATE"'", "to_date": "'"$TO_DATE"'", "enable_image_understanding": true}]
}'
```
Parse responses with `jq -r '.output[] | select(.type == "message") | .content[] | select(.type == "output_text") | .text'`.
**Path C — WebSearch fallback** (when both cache and `XAI_API_KEY` are unavailable, OR Grok returns nothing for a list):
Use the built-in WebSearch tool: `site:x.com "i/lists/${LIST_ID}" OR list:${LIST_ID} after:${FROM_DATE}`. Quality is lower; mark this list's source-status as `websearch` in step 6.
**Per-list outcome classification** (track for the source-status footer):
- `ok` — ≥3 tweets returned
- `quiet` — 1–2 tweets returned (list exists but a slow day)
- `empty` — 0 tweets returned (Grok found list but no posts in window)
- `error` — API/cache/Grok-can't-access failure (note the reason)
### 3. Build the candidate pool
Collect every tweet from every list into a single pool. For each tweet record: `{handle, text, likes, retweets, replies, views, url, list_ids_seen_on:[], list_names_seen_on:[], media, is_reply, is_quote}`.
**Deduplicate by URL across lists**: if the same tweet appears on multiple lists, merge the records — keep both `list_ids_seen_on` and `list_names_seen_on` populated. Cross-list appearance is a signal (used in step 4).
**Deduplicate against history**: drop any candidate whose URL is in `memory/list-digest-seen.txt` OR appears in the last 2 days of `memory/logs/`.
### 4. Score every candidate
For each surviving candidate, compute a **signal score**. Use natural log on engagement to prevent one viral tweet from dominating; add structural bonuses.
```
base = ln(1+likes) + 2.0*ln(1+retweets) + 1.5*ln(1+replies)
bonuses:
+2.0 if appeared on ≥2 distinct lists (cross-list resonance)
+1.5 if appeared on ≥3 distinct lists
+1.0 if topic_filter set AND tweet text/context matches it (case-insensitive substring or obvious semantic match)
+0.5 if author is small-account-signal (≤25k followers based on Grok's note OR you have no follower data — apply the bonus when the tweet *content* is technical/insider rather than influencer-style)
+0.3 if media is image OR video (richer artifact)
penalties:
-1.0 if is_reply AND replied-to is NOT on any tracked list (low context for the reader)
-0.5 if text is a pure link share with <10 words of original commentary
score = base + sum(bonuses) - sum(penalties)
```
### 5. Cluster into cross-list narratives
Group candidates into **narratives** when ALL three conditions hold:
- ≥2 tweets from ≥2 distinct lists
- shared ≥2 substantive keywords/entities (proper nouns, project names, ticker symbols, technical terms — ignore stop words)
- posted within the same 24h window
Each narrative gets a **narrative score** = sum of constituent tweet scores. Pick a one-line **narrative title** (≤80 chars) capturing what the cluster is collectively saying — e.g. "Anthropic releases Opus 4.7 — three lists pile in on agentic-eval results".
For each narrative, pick the **anchor tweet** (highest individual score) and up to 2 supporting tweets.
**Cluster-count cap**: if clustering produces fewer than 2 or more than 4 clusters, fall back to a flat ranked list with cluster labels inline (prepend each item with `[cluster-name]` when grouping would be informative, but do not emit the "🔗 Cross-list narratives" section).
### 6. Compose the digest
**Notification budget** (cap 4000 chars total):
- Up to **3 narratives** at the top, ranked by narrative score
- Then up to **5 standalone tweets per list** (highest individual score, not already in a narrative)
- Hard total cap of **12 items** across the whole digest — cut from the bottom of standalones if over
**Insight discipline**: every item must include a one-line **so-what** that adds context the tweet alone doesn't carry — the implication, the contrarian angle, the missing number, the deal-flow signal. A paraphrase ("@x said y") is not an insight and must be rewritten before sending.
**Quiet-list rule**: if a list's top surviving tweet has score < 2.0 (≈ <8 likes raw), write a one-line "quiet day" entry for that list rather than padding with low-signal items.
**Topic filter behavior**: when `TOPIC_FILTER` is set, it acts as a **scoring booster** (see step 4), NOT a hard filter. A list's top non-matching tweet still appears if it dominates on score; topic-matching tweets just get a thumb on the scale. Hard-filtering kills serendipity.
**Verdict line**: write one line at the very top capturing what today's lists collectively say. Examples:
- "Three lists piling in on the Opus 4.7 agentic-eval results; everything else is noise."
- "Slow day across all lists — only standout is @vitalik's MEV piece."
- "Crypto and AI lists diverging hard: AI lists locked on agent eval, crypto lists on Solana outage post-mortem."
### 7. Send the notification
Send via `./notify` (under 4000 chars total). Use the format below verbatim. Use `x.com/handle` (not `@handle`) to avoid pinging users on Telegram. Use Telegram Markdown link format `[label](url)`.
```
*List Digest — ${today}*
[VERDICT LINE — one line, ≤140 chars, plain text]
🔗 *Cross-list narratives*
1. *[narrative title]* — appeared on [List A] + [List B]
x.com/handle: [insight, not paraphrase] (♥ likes, ↻ rt) — [View](url)
x.com/handle2: [insight] (♥ likes, ↻ rt) — [View](url)
2. *[narrative title]* — appeared on [List A] + [List C]
...
*[List Name 1]*
- x.com/handle — [insight] (♥ likes, ↻ rt) — [View](url)
- x.com/handle — [insight] (♥ likes, ↻ rt) — [View](url)
*[List Name 2]*
- quiet day
---
sources: list1=ok | list2=quiet | list3=error(no-access)
status: LIST_DIGEST_OK
```
If `cross-list narratives` is empty, drop that whole section (don't print "(none)"). If every list is `quiet` or `empty`, send a single-line "*List Digest — ${today}* — quiet across all tracked lists" notification rather than padding.
### 8. Log and persist
Append to `memory/logs/$(date -u +%Y-%m-%d).md`:
```
## list-digest
- **Lists:** [count] tracked
- **Status:** LIST_DIGEST_OK | LIST_DIGEST_PARTIAL | LIST_DIGEST_EMPTY | LIST_DIGEST_NO_CONFIG
- **Per-list:** list1=ok(N) | list2=quiet(N) | list3=error
- **Verdict:** [verdict line]
- **Narratives:** [count] cross-list narratives
- **URLs reported:**
- https://x.com/handle1/status/...
- https://x.com/handle2/status/...
- ...
```
**Update the persistent seen-file**: append every reported tweet URL (one per line) to `memory/list-digest-seen.txt`. Create the file if missing. This survives log rotation and prevents stale tweets from cycling back.
**Exit-mode taxonomy**:
- `LIST_DIGEST_NO_CONFIG` — `${var}` empty/invalid OR no fetch path available. Log only, no notify.
- `LIST_DIGEST_EMPTY` — every list returned 0 tweets OR every candidate was already in seen-file. Log only, no notify.
- `LIST_DIGEST_PARTIAL` — some lists succeeded, some failed. Notify with whatever survived; surface failures in source-status footer.
- `LIST_DIGEST_OK` — ≥1 list returned ≥1 fresh tweet. Notify.
## Sandbox note
The sandbox may block outbound `curl`. Prefer the pre-fetched cache (`.xai-cache/list-digest-{LIST_ID}.json`); fall back to the X.AI API; fall back to **WebSearch** as the last resort. For auth-required APIs, see CLAUDE.md's pre-fetch / post-process pattern.
## Environment Variables Required
- `XAI_API_KEY` — X.AI API key for Grok `x_search`. If missing, the skill degrades to WebSearch (lower quality).
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!