diff --git a/src/server/signals/truth.test.ts b/src/server/signals/truth.test.ts
new file mode 100644
index 0000000..8087b7d
--- /dev/null
+++ b/src/server/signals/truth.test.ts
@@ -0,0 +1,32 @@
+import { describe, expect, test } from 'bun:test';
+import { matchCoins, parseTruthFeed } from './truth';
+
+const XML = `
');
+ });
+});
+
+describe('matchCoins', () => {
+ test('Name case-insensitive, Ticker nur exakt groß', () => {
+ expect(matchCoins('I love BITCOIN and solana')).toEqual(['BTC', 'SOL']);
+ expect(matchCoins('Buy ETH now')).toEqual(['ETH']);
+ expect(matchCoins('the ethics committee')).toEqual([]); // 'eth' klein/Teilwort matcht nicht
+ expect(matchCoins('Das sei seitwärts')).toEqual([]); // 'SEI' nur in Großschreibung
+ expect(matchCoins('THE ARENA IS PACKED')).toEqual([]); // 'ENA' nur mit Wortgrenze
+ expect(matchCoins('Tron will be huge')).toEqual(['TRX']); // nicht handelbar, aber Event
+ });
+ test('dedupliziert Mehrfach-Erwähnungen im selben Text', () => {
+ expect(matchCoins('BTC BTC Bitcoin')).toEqual(['BTC']);
+ });
+});
diff --git a/src/server/signals/truth.ts b/src/server/signals/truth.ts
new file mode 100644
index 0000000..a4f6bf8
--- /dev/null
+++ b/src/server/signals/truth.ts
@@ -0,0 +1,41 @@
+import { COIN_KEYWORDS } from './watchlist';
+
+export interface TruthPost {
+ url: string;
+ ts: number; // Unix ms
+ text: string;
+}
+
+const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+
+/** RSS-Items per Regex (kein XML-Parser nötig — Feed ist flach und stabil). */
+export function parseTruthFeed(xml: string): TruthPost[] {
+ const posts: TruthPost[] = [];
+ for (const item of xml.match(/