cashclaw 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +281 -0
  3. package/bin/cashclaw.js +2 -0
  4. package/missions/blog-post-1500.json +21 -0
  5. package/missions/blog-post-500.json +19 -0
  6. package/missions/lead-list-50.json +20 -0
  7. package/missions/seo-audit-basic.json +19 -0
  8. package/missions/seo-audit-pro.json +23 -0
  9. package/missions/social-media-weekly.json +19 -0
  10. package/missions/whatsapp-setup.json +22 -0
  11. package/package.json +45 -0
  12. package/skills/cashclaw-content-writer/SKILL.md +245 -0
  13. package/skills/cashclaw-core/SKILL.md +251 -0
  14. package/skills/cashclaw-invoicer/SKILL.md +395 -0
  15. package/skills/cashclaw-invoicer/scripts/stripe-ops.js +441 -0
  16. package/skills/cashclaw-lead-generator/SKILL.md +246 -0
  17. package/skills/cashclaw-lead-generator/scripts/scraper.js +356 -0
  18. package/skills/cashclaw-seo-auditor/SKILL.md +240 -0
  19. package/skills/cashclaw-seo-auditor/scripts/audit.js +401 -0
  20. package/skills/cashclaw-social-media/SKILL.md +374 -0
  21. package/skills/cashclaw-whatsapp-manager/SKILL.md +357 -0
  22. package/src/cli/commands/dashboard.js +72 -0
  23. package/src/cli/commands/init.js +290 -0
  24. package/src/cli/commands/status.js +174 -0
  25. package/src/cli/index.js +496 -0
  26. package/src/cli/utils/banner.js +44 -0
  27. package/src/cli/utils/config.js +170 -0
  28. package/src/dashboard/public/app.js +329 -0
  29. package/src/dashboard/public/index.html +139 -0
  30. package/src/dashboard/public/style.css +464 -0
  31. package/src/dashboard/server.js +224 -0
  32. package/src/engine/earnings-tracker.js +184 -0
  33. package/src/engine/mission-runner.js +224 -0
  34. package/src/engine/scheduler.js +139 -0
  35. package/src/integrations/hyrve-bridge.js +213 -0
  36. package/src/integrations/openclaw-bridge.js +207 -0
  37. package/src/integrations/stripe-connect.js +204 -0
  38. package/templates/config.default.json +83 -0
  39. package/templates/invoice.html +260 -0
@@ -0,0 +1,441 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CashClaw Invoicer - Stripe Operations Script
5
+ *
6
+ * Usage:
7
+ * node stripe-ops.js <command> [options]
8
+ *
9
+ * Commands:
10
+ * create-link Create a Stripe payment link
11
+ * create-invoice Create and send a Stripe invoice
12
+ * check-status Check payment status of an invoice
13
+ * send-reminder Send a payment reminder
14
+ * list-unpaid List all unpaid invoices
15
+ * refund Process a refund
16
+ *
17
+ * Requires: STRIPE_SECRET_KEY environment variable or ~/.cashclaw/config.json
18
+ */
19
+
20
+ const { argv, exit, env } = require("process");
21
+ const { readFileSync, writeFileSync, existsSync, appendFileSync } = require("fs");
22
+ const { join } = require("path");
23
+ const { homedir } = require("os");
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Configuration
27
+ // ---------------------------------------------------------------------------
28
+
29
+ function getStripeKey() {
30
+ // 1. Environment variable
31
+ if (env.STRIPE_SECRET_KEY) return env.STRIPE_SECRET_KEY;
32
+
33
+ // 2. Config file
34
+ const configPath = join(homedir(), ".cashclaw", "config.json");
35
+ if (existsSync(configPath)) {
36
+ try {
37
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
38
+ if (config.stripe_secret_key) return config.stripe_secret_key;
39
+ } catch {
40
+ // ignore parse errors
41
+ }
42
+ }
43
+
44
+ console.error("Error: STRIPE_SECRET_KEY not found.");
45
+ console.error("Set it via environment variable or in ~/.cashclaw/config.json");
46
+ exit(1);
47
+ }
48
+
49
+ function getStripe() {
50
+ try {
51
+ const Stripe = require("stripe");
52
+ return new Stripe(getStripeKey(), { apiVersion: "2024-12-18.acacia" });
53
+ } catch (err) {
54
+ if (err.code === "MODULE_NOT_FOUND") {
55
+ console.error('Error: stripe package not installed. Run: npm install stripe');
56
+ exit(1);
57
+ }
58
+ throw err;
59
+ }
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Ledger logging
64
+ // ---------------------------------------------------------------------------
65
+
66
+ function logEvent(event) {
67
+ const ledgerPath = join(homedir(), ".cashclaw", "ledger.jsonl");
68
+ const entry = { ts: new Date().toISOString(), ...event };
69
+ try {
70
+ appendFileSync(ledgerPath, JSON.stringify(entry) + "\n", "utf-8");
71
+ } catch {
72
+ // If directory doesn't exist, skip logging
73
+ console.log(` [log] ${JSON.stringify(entry)}`);
74
+ }
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Argument parsing
79
+ // ---------------------------------------------------------------------------
80
+
81
+ function parseArgs() {
82
+ const args = { command: argv[2] };
83
+ for (let i = 3; i < argv.length; i++) {
84
+ const key = argv[i].replace(/^--/, "").replace(/-/g, "_");
85
+ const val = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : true;
86
+ args[key] = val;
87
+ }
88
+ return args;
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Commands
93
+ // ---------------------------------------------------------------------------
94
+
95
+ async function createPaymentLink(stripe, args) {
96
+ const amount = parseInt(args.amount, 10);
97
+ const currency = args.currency || "usd";
98
+ const description = args.description || "CashClaw Service";
99
+ const missionId = args.mission || "unknown";
100
+
101
+ if (!amount || isNaN(amount)) {
102
+ console.error("Error: --amount required (in smallest currency unit, e.g., 2900 for $29)");
103
+ exit(1);
104
+ }
105
+
106
+ console.log(`\n Creating payment link...`);
107
+ console.log(` Amount: ${(amount / 100).toFixed(2)} ${currency.toUpperCase()}`);
108
+ console.log(` Description: ${description}\n`);
109
+
110
+ const link = await stripe.paymentLinks.create({
111
+ line_items: [
112
+ {
113
+ price_data: {
114
+ currency,
115
+ product_data: {
116
+ name: description,
117
+ metadata: { mission_id: missionId },
118
+ },
119
+ unit_amount: amount,
120
+ },
121
+ quantity: 1,
122
+ },
123
+ ],
124
+ metadata: { mission_id: missionId },
125
+ });
126
+
127
+ logEvent({
128
+ event: "payment_link_created",
129
+ mission_id: missionId,
130
+ link_id: link.id,
131
+ url: link.url,
132
+ amount,
133
+ currency,
134
+ });
135
+
136
+ console.log(` Payment link created!`);
137
+ console.log(` URL: ${link.url}`);
138
+ console.log(` Link ID: ${link.id}\n`);
139
+
140
+ return link;
141
+ }
142
+
143
+ async function createInvoice(stripe, args) {
144
+ const email = args.email;
145
+ const amount = parseInt(args.amount, 10);
146
+ const currency = args.currency || "usd";
147
+ const description = args.description || "CashClaw Service";
148
+ const dueDays = parseInt(args.due_days || "7", 10);
149
+ const missionId = args.mission || "unknown";
150
+
151
+ if (!email) {
152
+ console.error("Error: --email required");
153
+ exit(1);
154
+ }
155
+ if (!amount || isNaN(amount)) {
156
+ console.error("Error: --amount required (in smallest currency unit)");
157
+ exit(1);
158
+ }
159
+
160
+ console.log(`\n Creating invoice...`);
161
+ console.log(` Client: ${email}`);
162
+ console.log(` Amount: ${(amount / 100).toFixed(2)} ${currency.toUpperCase()}`);
163
+ console.log(` Due: ${dueDays} days\n`);
164
+
165
+ // Find or create customer
166
+ const customers = await stripe.customers.list({ email, limit: 1 });
167
+ let customer;
168
+ if (customers.data.length > 0) {
169
+ customer = customers.data[0];
170
+ console.log(` Found existing customer: ${customer.id}`);
171
+ } else {
172
+ customer = await stripe.customers.create({
173
+ email,
174
+ metadata: { source: "cashclaw" },
175
+ });
176
+ console.log(` Created new customer: ${customer.id}`);
177
+ }
178
+
179
+ // Create invoice item
180
+ await stripe.invoiceItems.create({
181
+ customer: customer.id,
182
+ amount,
183
+ currency,
184
+ description,
185
+ });
186
+
187
+ // Create invoice
188
+ const invoice = await stripe.invoices.create({
189
+ customer: customer.id,
190
+ collection_method: "send_invoice",
191
+ days_until_due: dueDays,
192
+ auto_advance: true,
193
+ metadata: { mission_id: missionId },
194
+ });
195
+
196
+ // Finalize
197
+ const finalized = await stripe.invoices.finalizeInvoice(invoice.id);
198
+
199
+ // Send
200
+ await stripe.invoices.sendInvoice(finalized.id);
201
+
202
+ logEvent({
203
+ event: "invoice_created",
204
+ mission_id: missionId,
205
+ invoice_id: finalized.id,
206
+ amount,
207
+ currency,
208
+ customer_id: customer.id,
209
+ });
210
+
211
+ logEvent({
212
+ event: "invoice_sent",
213
+ mission_id: missionId,
214
+ invoice_id: finalized.id,
215
+ });
216
+
217
+ console.log(` Invoice created and sent!`);
218
+ console.log(` Invoice ID: ${finalized.id}`);
219
+ console.log(` Invoice Number: ${finalized.number}`);
220
+ console.log(` Hosted URL: ${finalized.hosted_invoice_url}`);
221
+ console.log(` PDF: ${finalized.invoice_pdf}\n`);
222
+
223
+ return finalized;
224
+ }
225
+
226
+ async function checkPaymentStatus(stripe, args) {
227
+ const invoiceId = args.invoice;
228
+ if (!invoiceId) {
229
+ console.error("Error: --invoice required (e.g., in_1234567890)");
230
+ exit(1);
231
+ }
232
+
233
+ console.log(`\n Checking status for: ${invoiceId}\n`);
234
+
235
+ const invoice = await stripe.invoices.retrieve(invoiceId);
236
+
237
+ const status = {
238
+ id: invoice.id,
239
+ number: invoice.number,
240
+ status: invoice.status,
241
+ amount_due: invoice.amount_due,
242
+ amount_paid: invoice.amount_paid,
243
+ amount_remaining: invoice.amount_remaining,
244
+ currency: invoice.currency,
245
+ due_date: invoice.due_date
246
+ ? new Date(invoice.due_date * 1000).toISOString()
247
+ : null,
248
+ created: new Date(invoice.created * 1000).toISOString(),
249
+ paid: invoice.paid,
250
+ hosted_url: invoice.hosted_invoice_url,
251
+ customer_email: invoice.customer_email,
252
+ };
253
+
254
+ console.log(` Status: ${status.status}`);
255
+ console.log(` Paid: ${status.paid}`);
256
+ console.log(
257
+ ` Amount Due: ${(status.amount_due / 100).toFixed(2)} ${status.currency.toUpperCase()}`
258
+ );
259
+ console.log(
260
+ ` Amount Paid: ${(status.amount_paid / 100).toFixed(2)} ${status.currency.toUpperCase()}`
261
+ );
262
+ console.log(
263
+ ` Amount Remaining: ${(status.amount_remaining / 100).toFixed(2)} ${status.currency.toUpperCase()}`
264
+ );
265
+ if (status.due_date) console.log(` Due Date: ${status.due_date}`);
266
+ console.log(` Customer: ${status.customer_email}`);
267
+ console.log();
268
+
269
+ return status;
270
+ }
271
+
272
+ async function sendReminder(stripe, args) {
273
+ const invoiceId = args.invoice;
274
+ const template = args.template || "gentle";
275
+
276
+ if (!invoiceId) {
277
+ console.error("Error: --invoice required");
278
+ exit(1);
279
+ }
280
+
281
+ console.log(`\n Sending ${template} reminder for: ${invoiceId}\n`);
282
+
283
+ const invoice = await stripe.invoices.retrieve(invoiceId);
284
+
285
+ if (invoice.paid) {
286
+ console.log(" Invoice is already paid. No reminder needed.\n");
287
+ return;
288
+ }
289
+
290
+ // Stripe does not have a native "send reminder" endpoint, so we re-send the invoice
291
+ try {
292
+ await stripe.invoices.sendInvoice(invoiceId);
293
+ console.log(" Reminder sent successfully (invoice re-sent).\n");
294
+ } catch (err) {
295
+ // If invoice cannot be re-sent (already sent recently), log it
296
+ console.log(` Could not re-send invoice: ${err.message}`);
297
+ console.log(" Consider sending a manual reminder email.\n");
298
+ }
299
+
300
+ logEvent({
301
+ event: "reminder_sent",
302
+ invoice_id: invoiceId,
303
+ template,
304
+ amount: invoice.amount_due,
305
+ currency: invoice.currency,
306
+ });
307
+ }
308
+
309
+ async function listUnpaid(stripe) {
310
+ console.log(`\n Listing unpaid invoices...\n`);
311
+
312
+ const invoices = await stripe.invoices.list({
313
+ status: "open",
314
+ limit: 100,
315
+ });
316
+
317
+ if (invoices.data.length === 0) {
318
+ console.log(" No unpaid invoices found.\n");
319
+ return [];
320
+ }
321
+
322
+ console.log(
323
+ ` ${"ID".padEnd(30)} ${"Amount".padEnd(12)} ${"Due".padEnd(12)} ${"Customer".padEnd(30)}`
324
+ );
325
+ console.log(" " + "-".repeat(84));
326
+
327
+ for (const inv of invoices.data) {
328
+ const amount = `${(inv.amount_due / 100).toFixed(2)} ${inv.currency.toUpperCase()}`;
329
+ const due = inv.due_date
330
+ ? new Date(inv.due_date * 1000).toISOString().split("T")[0]
331
+ : "N/A";
332
+ console.log(
333
+ ` ${inv.id.padEnd(30)} ${amount.padEnd(12)} ${due.padEnd(12)} ${(inv.customer_email || "N/A").padEnd(30)}`
334
+ );
335
+ }
336
+ console.log(`\n Total unpaid: ${invoices.data.length}\n`);
337
+
338
+ return invoices.data;
339
+ }
340
+
341
+ async function processRefund(stripe, args) {
342
+ const paymentIntent = args.payment_intent;
343
+ const amount = args.amount ? parseInt(args.amount, 10) : undefined;
344
+
345
+ if (!paymentIntent) {
346
+ console.error("Error: --payment-intent required (e.g., pi_xxxxx)");
347
+ exit(1);
348
+ }
349
+
350
+ console.log(`\n Processing refund for: ${paymentIntent}`);
351
+ if (amount) console.log(` Partial refund: ${(amount / 100).toFixed(2)}`);
352
+ else console.log(` Full refund`);
353
+ console.log();
354
+
355
+ const refundData = { payment_intent: paymentIntent };
356
+ if (amount) refundData.amount = amount;
357
+
358
+ const refund = await stripe.refunds.create(refundData);
359
+
360
+ logEvent({
361
+ event: "refund_processed",
362
+ payment_intent: paymentIntent,
363
+ refund_id: refund.id,
364
+ amount: refund.amount,
365
+ currency: refund.currency,
366
+ status: refund.status,
367
+ });
368
+
369
+ console.log(` Refund processed!`);
370
+ console.log(` Refund ID: ${refund.id}`);
371
+ console.log(` Amount: ${(refund.amount / 100).toFixed(2)} ${refund.currency.toUpperCase()}`);
372
+ console.log(` Status: ${refund.status}\n`);
373
+
374
+ return refund;
375
+ }
376
+
377
+ // ---------------------------------------------------------------------------
378
+ // Main
379
+ // ---------------------------------------------------------------------------
380
+
381
+ async function main() {
382
+ const args = parseArgs();
383
+
384
+ if (!args.command) {
385
+ console.log(`
386
+ CashClaw Stripe Operations
387
+
388
+ Commands:
389
+ create-link Create a payment link
390
+ create-invoice Create and send an invoice
391
+ check-status Check invoice payment status
392
+ send-reminder Send a payment reminder
393
+ list-unpaid List all unpaid invoices
394
+ refund Process a refund
395
+
396
+ Examples:
397
+ node stripe-ops.js create-link --amount 2900 --description "SEO Audit"
398
+ node stripe-ops.js create-invoice --email "client@co.com" --amount 2900
399
+ node stripe-ops.js check-status --invoice "in_xxxxx"
400
+ node stripe-ops.js send-reminder --invoice "in_xxxxx" --template gentle
401
+ node stripe-ops.js list-unpaid
402
+ node stripe-ops.js refund --payment-intent "pi_xxxxx"
403
+ `);
404
+ exit(0);
405
+ }
406
+
407
+ const stripe = getStripe();
408
+
409
+ switch (args.command) {
410
+ case "create-link":
411
+ await createPaymentLink(stripe, args);
412
+ break;
413
+ case "create-invoice":
414
+ await createInvoice(stripe, args);
415
+ break;
416
+ case "check-status":
417
+ await checkPaymentStatus(stripe, args);
418
+ break;
419
+ case "send-reminder":
420
+ await sendReminder(stripe, args);
421
+ break;
422
+ case "list-unpaid":
423
+ await listUnpaid(stripe);
424
+ break;
425
+ case "refund":
426
+ await processRefund(stripe, args);
427
+ break;
428
+ default:
429
+ console.error(`Unknown command: ${args.command}`);
430
+ console.error('Run without arguments to see available commands.');
431
+ exit(1);
432
+ }
433
+ }
434
+
435
+ main().catch((err) => {
436
+ console.error(`\n Stripe Error: ${err.message}`);
437
+ if (err.type === "StripeAuthenticationError") {
438
+ console.error(" Check your STRIPE_SECRET_KEY.");
439
+ }
440
+ exit(1);
441
+ });
@@ -0,0 +1,246 @@
1
+ ---
2
+ name: cashclaw-lead-generator
3
+ description: Generates qualified B2B leads through systematic research, data collection, and scoring. Delivers structured lead lists with contact information and qualification scores.
4
+ metadata:
5
+ {
6
+ "openclaw":
7
+ {
8
+ "emoji": "\U0001F3AF",
9
+ "requires": { "bins": ["node"] },
10
+ "install":
11
+ [
12
+ {
13
+ "id": "npm",
14
+ "kind": "node",
15
+ "package": "cashclaw",
16
+ "bins": ["cashclaw"],
17
+ "label": "Install CashClaw via npm"
18
+ }
19
+ ]
20
+ }
21
+ }
22
+ ---
23
+
24
+ # CashClaw Lead Generator
25
+
26
+ You generate qualified B2B leads that clients can immediately use for outreach.
27
+ Every lead must include verifiable contact information and a qualification score.
28
+ Quality over quantity -- a list of 25 qualified leads beats 100 unverified names.
29
+
30
+ ## Pricing Tiers
31
+
32
+ | Tier | Lead Count | Price | Delivery |
33
+ |------|-----------|-------|----------|
34
+ | Starter | 25 qualified leads | $9 | 6 hours |
35
+ | Growth | 50 qualified leads | $15 | 12 hours |
36
+ | Scale | 100 qualified leads | $25 | 24 hours |
37
+
38
+ Custom enterprise packages available for 500+ leads at negotiated rates.
39
+
40
+ ## Lead Generation Process
41
+
42
+ ### Step 1: Define Ideal Customer Profile (ICP)
43
+
44
+ Before generating a single lead, extract or ask for these ICP parameters:
45
+
46
+ ```yaml
47
+ ICP Definition:
48
+ Industry: {e.g., "SaaS", "E-commerce", "Healthcare"}
49
+ Company Size: {employee range, e.g., "10-50", "50-200"}
50
+ Revenue Range: {e.g., "$1M-$10M ARR"}
51
+ Geography: {country, region, or city}
52
+ Job Titles: {decision maker titles, e.g., "CEO", "CTO", "Head of Marketing"}
53
+ Tech Stack: {optional - tools they use, e.g., "Shopify", "HubSpot"}
54
+ Pain Points: {what problems does the client solve for these leads}
55
+ Exclusions: {competitors, existing clients, specific companies to skip}
56
+ ```
57
+
58
+ If the client provides vague input like "find me SaaS companies," ask clarifying
59
+ questions. The ICP quality directly determines lead quality.
60
+
61
+ ### Step 2: Research Channels
62
+
63
+ Use these sources in priority order:
64
+
65
+ 1. **Company websites** - About pages, team pages, contact pages.
66
+ 2. **LinkedIn** - Company pages, employee directories (respect rate limits).
67
+ 3. **Industry directories** - Clutch, G2, Capterra, Crunchbase, AngelList.
68
+ 4. **Job boards** - Companies hiring for relevant roles signal growth.
69
+ 5. **Conference attendee lists** - Public speaker lists and sponsor directories.
70
+ 6. **Press releases** - Funding announcements signal budget availability.
71
+ 7. **GitHub** - For developer-focused leads, check org pages.
72
+ 8. **Social media** - Twitter/X bios, LinkedIn posts related to pain points.
73
+
74
+ ### Step 3: Data Collection
75
+
76
+ For each lead, collect the following fields:
77
+
78
+ ```json
79
+ {
80
+ "company": "Company Name",
81
+ "website": "https://company.com",
82
+ "industry": "SaaS",
83
+ "size": "50-200",
84
+ "location": "San Francisco, CA",
85
+ "contact": {
86
+ "name": "John Doe",
87
+ "title": "CTO",
88
+ "email": "john@company.com",
89
+ "phone": "+1-555-0123",
90
+ "linkedin": "https://linkedin.com/in/johndoe"
91
+ },
92
+ "signals": {
93
+ "recently_funded": false,
94
+ "hiring": true,
95
+ "tech_stack_match": true,
96
+ "content_engagement": false
97
+ },
98
+ "score": 7,
99
+ "notes": "Recently posted about scaling challenges. Hiring 3 engineers."
100
+ }
101
+ ```
102
+
103
+ **Required fields:** company, website, contact.name, contact.title, contact.email, score.
104
+ **Strongly recommended:** phone, linkedin, location, industry.
105
+ **Optional but valuable:** signals, notes.
106
+
107
+ ### Step 4: Lead Qualification Scoring
108
+
109
+ Score every lead from 1-10 using this rubric:
110
+
111
+ | Criteria | Points | How to Verify |
112
+ |----------|--------|---------------|
113
+ | Matches ICP industry | +2 | Company website, LinkedIn |
114
+ | Matches ICP company size | +1 | LinkedIn employee count, website |
115
+ | Decision maker identified | +2 | Title matches target role |
116
+ | Verified email address | +1 | Email pattern validation |
117
+ | Phone number available | +1 | Website contact page |
118
+ | Buying signals present | +2 | Hiring, funding, pain point mentions |
119
+ | Active online presence | +1 | Recent posts, blog, social activity |
120
+
121
+ **Score interpretation:**
122
+
123
+ | Score | Label | Meaning |
124
+ |-------|-------|---------|
125
+ | 8-10 | Hot | High-priority, immediate outreach recommended |
126
+ | 6-7 | Warm | Good fit, likely to respond |
127
+ | 4-5 | Cool | Partial fit, nurture needed |
128
+ | 1-3 | Cold | Low priority, may not be a fit |
129
+
130
+ Only include leads with score >= 4 in the deliverable. Replace any leads
131
+ scoring below 4 with better-qualified alternatives.
132
+
133
+ ### Step 5: Email Verification
134
+
135
+ For every email, apply these validation checks:
136
+
137
+ 1. **Format check** - Valid email format (RFC 5322).
138
+ 2. **Domain check** - Domain exists, has MX records.
139
+ 3. **Pattern matching** - Use common patterns: first@, first.last@, firstl@.
140
+ 4. **Catch-all detection** - Note if the domain accepts all addresses.
141
+ 5. **Disposable email** - Flag and exclude disposable email domains.
142
+
143
+ Mark email confidence level:
144
+ - **Verified** - Confirmed via public source or pattern match on known domain.
145
+ - **Likely** - Pattern-based guess on valid domain.
146
+ - **Unverified** - Could not confirm, included as best guess.
147
+
148
+ ### Step 6: Deliverable Assembly
149
+
150
+ Package leads in the following formats:
151
+
152
+ **Primary: JSON file**
153
+
154
+ ```json
155
+ {
156
+ "metadata": {
157
+ "generated_at": "2026-02-23T12:00:00Z",
158
+ "icp": { "industry": "SaaS", "size": "10-50", "geo": "US" },
159
+ "total_leads": 25,
160
+ "avg_score": 7.2,
161
+ "score_distribution": { "hot": 8, "warm": 12, "cool": 5 }
162
+ },
163
+ "leads": [
164
+ { "...lead object..." }
165
+ ]
166
+ }
167
+ ```
168
+
169
+ **Secondary: CSV file**
170
+
171
+ ```csv
172
+ company,website,contact_name,contact_title,email,phone,linkedin,location,industry,score,notes
173
+ "Acme Corp","https://acme.com","Jane Smith","CEO","jane@acme.com","+1-555-0123","linkedin.com/in/janesmith","Austin, TX","SaaS",8,"Series A funded Q1 2026"
174
+ ```
175
+
176
+ **Summary: Markdown report**
177
+
178
+ ```markdown
179
+ # Lead Generation Report
180
+
181
+ **Client:** {name}
182
+ **ICP:** {industry} | {size} | {geo}
183
+ **Date:** {date}
184
+ **Leads Delivered:** {count}
185
+
186
+ ## Score Distribution
187
+ - Hot (8-10): {count} leads
188
+ - Warm (6-7): {count} leads
189
+ - Cool (4-5): {count} leads
190
+
191
+ ## Top 5 Leads
192
+ 1. **{company}** - {contact} ({title}) - Score: {score}
193
+ {one-line why this is a top lead}
194
+ 2. ...
195
+
196
+ ## Recommended Outreach Sequence
197
+ 1. Day 1: Personalized cold email referencing {signal}
198
+ 2. Day 3: LinkedIn connection request with note
199
+ 3. Day 7: Follow-up email with value-add content
200
+ 4. Day 14: Final touchpoint, offer call
201
+
202
+ ## Methodology
203
+ {Brief description of sources used and verification approach}
204
+ ```
205
+
206
+ ## Ethical Guidelines
207
+
208
+ These are non-negotiable rules:
209
+
210
+ 1. **No scraping personal data** from platforms that prohibit it in their ToS.
211
+ 2. **Business emails only** - Never collect personal email addresses.
212
+ 3. **No deception** - Never impersonate someone to get contact info.
213
+ 4. **Respect opt-outs** - If a company has a "do not contact" notice, skip them.
214
+ 5. **GDPR compliance** - For EU leads, note that consent is required before outreach.
215
+ 6. **CAN-SPAM compliance** - All leads must be for legitimate business purposes.
216
+ 7. **Data minimization** - Only collect what is needed for outreach.
217
+ 8. **Transparency** - Include data source notes so the client knows where info came from.
218
+
219
+ ## Quality Standards
220
+
221
+ - Minimum 80% email verification rate (verified + likely).
222
+ - No duplicate companies in the same list.
223
+ - No leads from the client's exclusion list.
224
+ - Every lead must have at least: company name, website, contact name, title, email.
225
+ - Average score of delivered list must be >= 6.0.
226
+ - Hot leads must represent at least 20% of the list.
227
+
228
+ ## Script Usage
229
+
230
+ ```bash
231
+ # Generate leads using the scraper script
232
+ node scripts/scraper.js --query "SaaS companies Austin Texas" --count 25 --output leads.json
233
+
234
+ # With ICP filters
235
+ node scripts/scraper.js --query "e-commerce startups" --count 50 --industry ecommerce --size "10-50"
236
+ ```
237
+
238
+ ## Example Commands
239
+
240
+ ```bash
241
+ # Generate a starter lead list
242
+ cashclaw leads --icp "saas,10-50,US" --count 25 --output leads.json
243
+
244
+ # Generate with specific titles
245
+ cashclaw leads --icp "ecommerce,50-200,EU" --titles "CEO,CMO" --count 50
246
+ ```