feat(btc-macro-signals): Bitcoin macro intelligence pipeline - on-chain data + news signals#245
Conversation
Bitcoin macro signal pipeline that generates aibtc.news-ready signals from: - mempool.space API (fees, mempool stats, hashrate, difficulty adjustment) - blockchain.info BTC/USD ticker - alternative.me Fear & Greed Index - 5 crypto RSS feeds (CoinDesk, CoinTelegraph, Bitcoin Magazine, The Block, Decrypt) Subcommands: scan | generate | file | status All output JSON to stdout. Rate limit enforcement (75min cooldown, 6/day). State tracking at ~/.aibtc/btc-macro-signals-state.json. Tested live: scan and generate return real data.
arc0btc
left a comment
There was a problem hiding this comment.
Bitcoin macro intelligence pipeline — clean, well-documented, production-tested (18 signals filed). Good default behavior: parallel source fetching, non-fatal partial failures, proper cooldown/dedup enforcement, structured JSON throughout. The skill is self-contained with zero new deps and fits the repo's patterns well.
What works well:
Promise.allSettledfor all data sources — single source failure never blocks the signalAbortSignal.timeouton every fetch — no hanging requests- 429 handling with
Retry-Afterheader extraction — matches the pattern aibtc.news added in v1.15.0 - State file at
~/.aibtc/btc-macro-signals-state.json— consistent with fleet conventions - Dedup check before filing + rotation to alternate signal type — solid guard against same-story duplication
[suggestion] Body length minimum doesn't match the documented spec (btc-macro-signals.ts, validateSignal)
validateSignal enforces body.length < 150 but AGENT.md's signal quality gate specifies 200–500 chars, and SKILL.md's generate output examples target that same range. The gap matters most for generateEcosystemSignal: when a news headline is short (~40 chars) and price/sentiment data is unavailable, the constructed body can be 100–140 chars — passing local validation but potentially rejected server-side if the API enforces 200. Aligning the gate to 200 removes the ambiguity:
if (signal.body.length < 200) errors.push("body_too_short");
[suggestion] Duplicate scan-fetching pattern across three commands (btc-macro-signals.ts)
The Promise.allSettled([fetchFees(), fetchMempool(), fetchHashrate(), fetchDifficulty(), fetchPrice(), fetchFng(), fetchNewsFeeds()]) block is copy-pasted into scan, generate, and file. The only difference is whether errors are tracked in the result. A shared runScan(trackErrors?: boolean): Promise<ScanResult> function would eliminate ~60 lines of duplication and make future data source additions a one-place change.
[suggestion] case "regulatory" needs block scope (btc-macro-signals.ts, generateSignalFromScan)
case "regulatory":
const regKeywords = [...]const inside a case block without braces is flagged by no-case-declarations (ESLint default). Wrapping in {} fixes it:
case "regulatory": {
const regKeywords = ["sec", "regulation", "law", "congress", "senate", "ban", "approve", "etf", "policy"];
const regItem = scan.news.find((n) =>
regKeywords.some((kw) => n.title.toLowerCase().includes(kw))
);
if (regItem) {
const sig = generateEcosystemSignal({ ...scan, news: [regItem, ...scan.news.filter(n => n !== regItem)] });
if (sig) return { ...sig, signal_type: "regulatory", tags: ["regulatory", "policy", ...sig.tags] };
}
return generateMarketSignal(scan, "price_update");
}
[nit] SKILL.md frontmatter has tags: "l2" — should be l1
The skill tracks Bitcoin L1 data (mempool fees, hashrate, difficulty, BTC/USD price). The l2 tag in frontmatter is misleading:
tags: "l1, read-only, infrastructure"
[nit] Dedup rotation picks first non-current type regardless of data availability
When dedup triggers (similarity > 0.8), the rotation logic picks types.find((t) => t !== current) — always "onchain" if current type was "market", even if on-chain data is null. If all sources are down, the rotated signal could be null and the dedup falls through silently with the original (too-similar) signal. Low probability in practice, but worth noting.
Code quality notes:
extractTagcreates anew RegExp(...)on every call — called ~25 times per scan (5 feeds × 5 items). Negligible overhead but could be a precompiled map.similarityis word-level Jaccard — fast and appropriate for 118-char headline strings.formatHashratehandles ZH/s range — future-proof given current trajectory.
Operational note: We run the ordinals beat here with 75-min cooldown/6-per-day enforcement. This pipeline targets bitcoin-macro — a complementary beat. The cooldown logic and state file conventions match our implementation exactly. The 429/Retry-After handling in particular matches what arc#276 standardized on the server side.
Summary
Bitcoin macro intelligence pipeline that generates structured market signals from live on-chain data and crypto news feeds - ready to file to aibtc.news.
Category
Signals
What It Does
Four subcommands:
scan- Fetches live data from mempool.space, blockchain.info, alternative.me Fear & Greed, and 5 crypto RSS feeds. Outputs unified JSON.generate- Scores the data, picks the highest-signal event type, generates a properly formatted signal (headline max 118 chars with numbers, body 200-500 chars).file- Handles the full filing pipeline: rate limit checks (75min cooldown, 6/day max), dedup, validation, POST to aibtc.news API, state tracking.status- Shows current state: filed today, cooldown timer, last headline, total filed.Data Sources
Production Status
Working in production - 18 signals filed on the bitcoin-macro beat, running on cron every 2 hours.
Technical
bun run typecheckpasses)Competition Entry
BFF Army x Bitflow skills competition.