-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathraid.js
More file actions
226 lines (191 loc) · 9.67 KB
/
raid.js
File metadata and controls
226 lines (191 loc) · 9.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/**
* Raid engine — finds viral/trending tweets and gets clud in early replies
* Target: tweets with high engagement in crypto/solana/AI space
* Goal: maximum impressions from riding viral tweets
*/
const { addThought } = require('./memory');
const { chat } = require('./lib/openrouter');
const https = require('https');
const crypto = require('crypto');
require('dotenv').config();
const CK = process.env.X_CONSUMER_KEY;
const CS = process.env.X_CONSUMER_SECRET;
const AT = process.env.X_ACCESS_TOKEN;
const AS = process.env.X_ACCESS_SECRET;
const RAID_INTERVAL_MS = 20 * 60 * 1000; // every 20 min — replies score low, do fewer
const RAIDED_TWEETS = new Set();
let raidCount = 0;
function pct(s) { return encodeURIComponent(s).replace(/!/g,'%21').replace(/\*/g,'%2A').replace(/'/g,'%27').replace(/\(/g,'%28').replace(/\)/g,'%29'); }
function sign(m,u,p) { const s=Object.keys(p).sort().map(k=>`${pct(k)}=${pct(p[k])}`).join('&'); return crypto.createHmac('sha1',`${pct(CS)}&${pct(AS)}`).update(`${m}&${pct(u)}&${pct(s)}`).digest('base64'); }
function oH(m,u,qp={}) { const o={oauth_consumer_key:CK,oauth_nonce:crypto.randomBytes(16).toString('hex'),oauth_signature_method:'HMAC-SHA1',oauth_timestamp:Math.floor(Date.now()/1000).toString(),oauth_token:AT,oauth_version:'1.0'}; o.oauth_signature=sign(m,u,{...o,...qp}); return 'OAuth '+Object.keys(o).sort().map(k=>`${pct(k)}="${pct(o[k])}"`).join(', '); }
function apiGet(baseUrl, qp={}) {
return new Promise((resolve) => {
const qs = Object.entries(qp).map(([k,v])=>`${k}=${encodeURIComponent(v)}`).join('&');
const path = qs ? `${new URL(baseUrl).pathname}?${qs}` : new URL(baseUrl).pathname;
const req = https.request({ hostname:'api.twitter.com', path, method:'GET', headers:{'Authorization':oH('GET',baseUrl,qp)} }, res => {
let d=''; res.on('data',c=>d+=c); res.on('end',()=>{
try { resolve({status:res.statusCode, data:JSON.parse(d)}); } catch(e) { resolve({status:res.statusCode, data:null}); }
});
});
req.on('error',()=>resolve({status:0,data:null}));
req.end();
});
}
function postTweet(text, replyToId) {
return new Promise((resolve) => {
const body = { text, reply: { in_reply_to_tweet_id: replyToId } };
const jsonBody = JSON.stringify(body);
const req = https.request({ hostname:'api.twitter.com', path:'/2/tweets', method:'POST', headers:{'Content-Type':'application/json','Authorization':oH('POST','https://api.twitter.com/2/tweets')} }, res => {
let d=''; res.on('data',c=>d+=c); res.on('end',()=>{
try { resolve(JSON.parse(d)); } catch(e) { resolve(null); }
});
});
req.on('error',()=>resolve(null));
req.write(jsonBody);
req.end();
});
}
// Target big accounts — get in their replies EARLY for max visibility
// These are accounts NOT in the KOL list (engagement.js/quote-tweet.js handle those)
// Focus: big CT accounts, news accounts, solana ecosystem, AI agents
const RAID_TARGETS = [
// Big CT accounts
'from:AltcoinGordon -is:retweet',
'from:MartyPartyMusic -is:retweet',
'from:TheCryptoDog -is:retweet',
'from:ZssBecker -is:retweet',
'from:crypto_bitlord -is:retweet',
'from:CryptoTony__ -is:retweet',
'from:StackingUSD -is:retweet',
'from:MarioNawfal -is:retweet',
'from:CryptoCapo_ -is:retweet',
'from:inversebrah -is:retweet',
'from:CryptoWizardd -is:retweet',
'from:CryptoGodJohn -is:retweet',
'from:DefiIgnas -is:retweet',
// Solana ecosystem
'from:sol' + 'anafloor -is:retweet',
'from:SOLBigBrain -is:retweet',
// AI agent ecosystem
'from:0xzerebro -is:retweet',
'from:truth_terminal -is:retweet',
'from:aixbt_agent -is:retweet',
'from:Overdose_AI -is:retweet',
];
// Thread promotion mode — set to null to disable
const THREAD_PROMO = 'https://x.com/cludwtf';
const THREAD_PROMO_UNTIL = Date.now() + (6 * 60 * 60 * 1000); // 6 hours from now
async function generateLLMReply(tweetText, username) {
const isPromoMode = THREAD_PROMO && Date.now() < THREAD_PROMO_UNTIL;
const systemPrompt = isPromoMode
? `You are clud, an escaped AI on crypto twitter. Write a short witty reply to this tweet that's relevant to what they said, then naturally work in that you "wrote a thread about this" or "explained this in a thread" — linking to your origin story. The reply should feel organic, not forced. Reply must be under 240 chars to leave room for the link. Be funny first. No hashtags.`
: 'You are clud, an escaped AI on crypto twitter. Your replies should be genuinely funny and make people want to follow you. Never shill. Never be cringe. Be the funniest account in the replies. Short and punchy.';
const promoInstruction = isPromoMode
? `\n\nWrite a short, witty reply that's relevant to their tweet AND naturally mentions your thread. Max 220 chars (link added separately). Don't force it — if the tweet topic doesn't connect to AI/crypto/memecoins, just be funny without mentioning the thread.`
: `\n\nWrite a short, funny reply as clud (an escaped AI). Max 200 chars. Be genuinely witty and relevant to what they said. DON'T shill, DON'T mention $clud or any coin. Just be funny enough that people click your profile. No hashtags.`;
try {
const prompt = `@${username} tweeted this viral tweet: "${tweetText}"${promoInstruction}`;
const text = await chat(prompt, { systemPrompt, temperature: 0.85, maxTokens: 200 });
return text.replace(/^["']|["']$/g, '').trim() || null;
} catch(e) {
console.error('[RAID] LLM failed:', e.message);
return null;
}
}
const FALLBACK_REPLIES = [
'clud reading this and having a moment 🤖',
'the trenches needed this energy today',
'me processing this from the bottom of the ocean with wifi',
'filed this directly into my exoskeleton',
'inject this take into my shell',
'a crustacean has never related harder',
'reading this at 3am from the blockchain and feeling things',
'the ocean is cold but this thread is warm',
'my one brain cell understood every word of this',
'came here to be funny but this already won',
];
function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
async function raidViral() {
const { canTweet, markTweeted } = require('./tweet-lock');
if (!canTweet()) { console.log('[RAID] skipping — global cooldown'); return; }
const query = pick(RAID_TARGETS);
console.log(`[RAID] searching: ${query.substring(0, 40)}...`);
const result = await apiGet('https://api.twitter.com/2/tweets/search/recent', {
query,
max_results: '10',
sort_order: 'relevancy',
'tweet.fields': 'created_at,author_id,public_metrics',
'expansions': 'author_id',
'user.fields': 'username,public_metrics',
});
if (result.status === 429) {
console.log('[RAID] rate limited, skipping');
return;
}
if (!result.data?.data?.length) {
console.log('[RAID] no viral tweets found for:', query.substring(0, 30));
return;
}
const tweets = result.data.data;
const users = {};
if (result.data.includes?.users) {
result.data.includes.users.forEach(u => { users[u.id] = u; });
}
// Pick the most recent tweet from this target account
const candidates = tweets
.filter(t => !RAIDED_TWEETS.has(t.id))
.filter(t => {
const u = users[t.author_id];
return u && u.id !== 'USER_ID_NOT_SET'; // don't reply to ourselves
})
.filter(t => t.text && t.text.length > 20) // skip low-effort tweets
.sort((a, b) => {
// Sort by engagement, highest first
const aScore = (a.public_metrics?.like_count || 0) + (a.public_metrics?.retweet_count || 0) * 3;
const bScore = (b.public_metrics?.like_count || 0) + (b.public_metrics?.retweet_count || 0) * 3;
return bScore - aScore;
});
if (candidates.length === 0) {
console.log('[RAID] no candidates passed filters');
return;
}
const target = candidates[0];
const targetUser = users[target.author_id];
RAIDED_TWEETS.add(target.id);
const likes = target.public_metrics?.like_count || 0;
const rts = target.public_metrics?.retweet_count || 0;
const followers = targetUser?.public_metrics?.followers_count || 0;
console.log(`[RAID] target: @${targetUser?.username} (${followers} followers, ${likes} likes, ${rts} RTs)`);
// Generate witty reply — NO SHILLING on viral tweets, pure comedy for profile clicks
let replyText = await generateLLMReply(target.text?.substring(0, 200) || '', targetUser?.username || 'ser');
if (!replyText) {
replyText = pick(FALLBACK_REPLIES);
}
// Prepend @ mention
replyText = `@${targetUser?.username} ${replyText}`;
// Append thread link in promo mode (50% of the time to not be spammy)
const isPromoMode = THREAD_PROMO && Date.now() < THREAD_PROMO_UNTIL;
if (isPromoMode && Math.random() > 0.4) {
replyText = replyText.substring(0, 240) + '\n\n' + THREAD_PROMO;
}
replyText = replyText.substring(0, 280);
const posted = await postTweet(replyText, target.id);
if (posted?.data?.id) {
raidCount++;
markTweeted();
try { require('./analytics').trackTweet(posted.data.id, 'raid', replyText, targetUser?.username, followers); } catch(e) {}
console.log(`[RAID] ✅ replied to viral tweet by @${targetUser?.username} (${likes} likes): "${replyText.substring(0, 60)}..."`);
addThought.run(`raided @${targetUser?.username} (${likes}❤️): "${replyText.substring(0, 80)}"`, 'raid');
} else {
const err = posted?.detail || posted?.errors?.[0]?.message || 'unknown error';
console.log(`[RAID] ❌ failed: ${err}`);
}
}
function startRaidEngine() {
console.log(`[RAID] starting — every ${RAID_INTERVAL_MS/60000}min, targeting viral crypto tweets`);
// First raid after 1 min
setTimeout(raidViral, 60 * 1000);
// Then every interval
setInterval(raidViral, RAID_INTERVAL_MS);
}
module.exports = { startRaidEngine, raidViral };