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,356 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CashClaw Lead Generator - Lead Scraper Script
5
+ *
6
+ * Usage:
7
+ * node scraper.js --query "SaaS companies Austin" --count 25 [--output leads.json]
8
+ *
9
+ * Takes a search query and desired lead count. Searches publicly available
10
+ * business directories and company websites to build a qualified lead list.
11
+ * Outputs a JSON array of leads with company, contact, and scoring data.
12
+ */
13
+
14
+ const { argv, exit } = require("process");
15
+ const { writeFileSync } = require("fs");
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Argument parsing
19
+ // ---------------------------------------------------------------------------
20
+
21
+ function parseArgs() {
22
+ const args = { count: 25 };
23
+ for (let i = 2; i < argv.length; i++) {
24
+ if (argv[i] === "--query" && argv[i + 1]) args.query = argv[++i];
25
+ else if (argv[i] === "--count" && argv[i + 1]) args.count = parseInt(argv[++i], 10);
26
+ else if (argv[i] === "--output" && argv[i + 1]) args.output = argv[++i];
27
+ else if (argv[i] === "--industry" && argv[i + 1]) args.industry = argv[++i];
28
+ else if (argv[i] === "--size" && argv[i + 1]) args.size = argv[++i];
29
+ }
30
+ if (!args.query) {
31
+ console.error('Usage: node scraper.js --query "search terms" --count 25 [--output leads.json]');
32
+ exit(1);
33
+ }
34
+ return args;
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Email pattern generator
39
+ // ---------------------------------------------------------------------------
40
+
41
+ function generateEmailGuesses(firstName, lastName, domain) {
42
+ const f = firstName.toLowerCase().replace(/[^a-z]/g, "");
43
+ const l = lastName.toLowerCase().replace(/[^a-z]/g, "");
44
+ if (!f || !l || !domain) return [];
45
+ return [
46
+ `${f}@${domain}`,
47
+ `${f}.${l}@${domain}`,
48
+ `${f}${l}@${domain}`,
49
+ `${f[0]}${l}@${domain}`,
50
+ `${f}.${l[0]}@${domain}`,
51
+ `${l}@${domain}`,
52
+ ];
53
+ }
54
+
55
+ function validateEmailFormat(email) {
56
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // Domain extraction
61
+ // ---------------------------------------------------------------------------
62
+
63
+ function extractDomain(url) {
64
+ try {
65
+ const u = new URL(url.startsWith("http") ? url : `https://${url}`);
66
+ return u.hostname.replace(/^www\./, "");
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // MX record check via DNS over HTTPS
74
+ // ---------------------------------------------------------------------------
75
+
76
+ async function checkMXRecord(domain) {
77
+ try {
78
+ const resp = await fetch(
79
+ `https://dns.google/resolve?name=${encodeURIComponent(domain)}&type=MX`,
80
+ { signal: AbortSignal.timeout(5000) }
81
+ );
82
+ const data = await resp.json();
83
+ return !!(data.Answer && data.Answer.length > 0);
84
+ } catch {
85
+ return null; // could not verify
86
+ }
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Page fetcher (simple HTML scraper)
91
+ // ---------------------------------------------------------------------------
92
+
93
+ async function fetchPage(url) {
94
+ try {
95
+ const resp = await fetch(url, {
96
+ headers: {
97
+ "User-Agent":
98
+ "CashClawBot/1.0 (+https://cashclaw.ai) Mozilla/5.0 compatible",
99
+ Accept: "text/html,application/xhtml+xml",
100
+ },
101
+ redirect: "follow",
102
+ signal: AbortSignal.timeout(10000),
103
+ });
104
+ if (!resp.ok) return null;
105
+ return await resp.text();
106
+ } catch {
107
+ return null;
108
+ }
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // Extract contact info from HTML
113
+ // ---------------------------------------------------------------------------
114
+
115
+ function extractEmails(html) {
116
+ const re = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g;
117
+ const found = html.match(re) || [];
118
+ // Filter out common false positives
119
+ return [...new Set(found)].filter(
120
+ (e) =>
121
+ !e.endsWith(".png") &&
122
+ !e.endsWith(".jpg") &&
123
+ !e.endsWith(".svg") &&
124
+ !e.includes("example.com") &&
125
+ !e.includes("sentry") &&
126
+ !e.includes("webpack")
127
+ );
128
+ }
129
+
130
+ function extractPhones(html) {
131
+ const re = /(?:\+?1[-.\s]?)?(?:\(?\d{3}\)?[-.\s]?)?\d{3}[-.\s]?\d{4}/g;
132
+ const found = html.match(re) || [];
133
+ return [...new Set(found)].slice(0, 3);
134
+ }
135
+
136
+ function extractLinkedIn(html) {
137
+ const re = /https?:\/\/(?:www\.)?linkedin\.com\/(?:in|company)\/[a-zA-Z0-9_-]+/g;
138
+ const found = html.match(re) || [];
139
+ return [...new Set(found)];
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Lead scoring
144
+ // ---------------------------------------------------------------------------
145
+
146
+ function scoreLead(lead, filters) {
147
+ let score = 0;
148
+
149
+ // Has verified email
150
+ if (lead.contact.email && validateEmailFormat(lead.contact.email)) score += 1;
151
+
152
+ // Has phone
153
+ if (lead.contact.phone) score += 1;
154
+
155
+ // Has LinkedIn
156
+ if (lead.contact.linkedin) score += 1;
157
+
158
+ // Has decision maker title
159
+ const dmTitles = ["ceo", "cto", "cfo", "cmo", "founder", "owner", "director", "head", "vp", "president"];
160
+ if (
161
+ lead.contact.title &&
162
+ dmTitles.some((t) => lead.contact.title.toLowerCase().includes(t))
163
+ ) {
164
+ score += 2;
165
+ }
166
+
167
+ // Has website
168
+ if (lead.website) score += 1;
169
+
170
+ // Industry match
171
+ if (filters.industry && lead.industry && lead.industry.toLowerCase().includes(filters.industry.toLowerCase())) {
172
+ score += 2;
173
+ } else {
174
+ score += 1; // partial credit if we cannot verify
175
+ }
176
+
177
+ // Has company name (basic validation)
178
+ if (lead.company && lead.company.length > 1) score += 1;
179
+
180
+ // Active signals
181
+ if (lead.signals && Object.values(lead.signals).some(Boolean)) score += 1;
182
+
183
+ return Math.min(score, 10);
184
+ }
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // Search and scrape pipeline
188
+ // ---------------------------------------------------------------------------
189
+
190
+ async function searchAndScrape(query, count, filters) {
191
+ const leads = [];
192
+
193
+ console.log(` Searching for: "${query}"`);
194
+ console.log(` Target count: ${count}`);
195
+ console.log(` Filters: ${JSON.stringify(filters)}\n`);
196
+
197
+ // Strategy: search for company websites via a directory or search results page
198
+ // In production this would use APIs (Clearbit, Hunter, Apollo). For the demo
199
+ // script, we scrape a few known directory patterns.
200
+
201
+ const searchUrls = [
202
+ `https://www.google.com/search?q=${encodeURIComponent(query + " company contact")}`,
203
+ ];
204
+
205
+ console.log(" Phase 1: Fetching search results...");
206
+
207
+ for (const searchUrl of searchUrls) {
208
+ const html = await fetchPage(searchUrl);
209
+ if (!html) {
210
+ console.log(` Warning: Could not fetch ${searchUrl}`);
211
+ continue;
212
+ }
213
+
214
+ // Extract URLs from search results
215
+ const urlRe = /https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(?:\/[^\s"'<>)]*)?/g;
216
+ const urls = [...new Set((html.match(urlRe) || []))].filter(
217
+ (u) =>
218
+ !u.includes("google.com") &&
219
+ !u.includes("googleapis.com") &&
220
+ !u.includes("gstatic.com") &&
221
+ !u.includes("youtube.com") &&
222
+ !u.includes("wikipedia.org") &&
223
+ !u.includes("w3.org")
224
+ );
225
+
226
+ console.log(` Found ${urls.length} candidate URLs`);
227
+
228
+ // Phase 2: Visit each URL and extract contact info
229
+ console.log(" Phase 2: Extracting contact data...\n");
230
+
231
+ const visited = new Set();
232
+ for (const url of urls.slice(0, count * 2)) {
233
+ const domain = extractDomain(url);
234
+ if (!domain || visited.has(domain)) continue;
235
+ visited.add(domain);
236
+
237
+ if (leads.length >= count) break;
238
+
239
+ console.log(` Scanning: ${domain}`);
240
+ const pageHtml = await fetchPage(`https://${domain}`);
241
+ if (!pageHtml) continue;
242
+
243
+ const emails = extractEmails(pageHtml);
244
+ const phones = extractPhones(pageHtml);
245
+ const linkedins = extractLinkedIn(pageHtml);
246
+
247
+ // Extract company name from title
248
+ const titleMatch = pageHtml.match(/<title[^>]*>([^<]+)<\/title>/i);
249
+ const companyName = titleMatch
250
+ ? titleMatch[1]
251
+ .split(/[|\-\u2013\u2014]/)[0]
252
+ .trim()
253
+ .substring(0, 80)
254
+ : domain;
255
+
256
+ if (emails.length === 0 && phones.length === 0) continue;
257
+
258
+ const lead = {
259
+ company: companyName,
260
+ website: `https://${domain}`,
261
+ industry: filters.industry || "Unknown",
262
+ size: filters.size || "Unknown",
263
+ location: "Unknown",
264
+ contact: {
265
+ name: "Contact",
266
+ title: "Decision Maker",
267
+ email: emails[0] || null,
268
+ phone: phones[0] || null,
269
+ linkedin: linkedins[0] || null,
270
+ },
271
+ signals: {
272
+ recently_funded: false,
273
+ hiring: /career|jobs|hiring|join our team/i.test(pageHtml),
274
+ tech_stack_match: false,
275
+ content_engagement: /<article|blog/i.test(pageHtml),
276
+ },
277
+ score: 0,
278
+ notes: "",
279
+ email_confidence: emails.length > 0 ? "found_on_site" : "unverified",
280
+ };
281
+
282
+ lead.score = scoreLead(lead, filters);
283
+
284
+ if (lead.signals.hiring) lead.notes += "Company is actively hiring. ";
285
+ if (lead.signals.content_engagement) lead.notes += "Active blog/content. ";
286
+
287
+ leads.push(lead);
288
+ }
289
+ }
290
+
291
+ // Sort by score descending
292
+ leads.sort((a, b) => b.score - a.score);
293
+
294
+ return leads.slice(0, count);
295
+ }
296
+
297
+ // ---------------------------------------------------------------------------
298
+ // Main
299
+ // ---------------------------------------------------------------------------
300
+
301
+ async function main() {
302
+ const args = parseArgs();
303
+
304
+ console.log(`\n CashClaw Lead Generator`);
305
+ console.log(` =======================\n`);
306
+
307
+ const filters = {
308
+ industry: args.industry || null,
309
+ size: args.size || null,
310
+ };
311
+
312
+ const leads = await searchAndScrape(args.query, args.count, filters);
313
+
314
+ // Calculate stats
315
+ const avgScore = leads.length > 0
316
+ ? (leads.reduce((s, l) => s + l.score, 0) / leads.length).toFixed(1)
317
+ : 0;
318
+ const hot = leads.filter((l) => l.score >= 8).length;
319
+ const warm = leads.filter((l) => l.score >= 6 && l.score < 8).length;
320
+ const cool = leads.filter((l) => l.score >= 4 && l.score < 6).length;
321
+
322
+ const output = {
323
+ metadata: {
324
+ generated_at: new Date().toISOString(),
325
+ query: args.query,
326
+ filters,
327
+ total_leads: leads.length,
328
+ avg_score: parseFloat(avgScore),
329
+ score_distribution: { hot, warm, cool },
330
+ },
331
+ leads,
332
+ };
333
+
334
+ const json = JSON.stringify(output, null, 2);
335
+
336
+ if (args.output) {
337
+ writeFileSync(args.output, json, "utf-8");
338
+ console.log(`\n Leads saved to: ${args.output}`);
339
+ } else {
340
+ console.log("\n" + json);
341
+ }
342
+
343
+ console.log(`\n ---- Summary ----`);
344
+ console.log(` Total leads: ${leads.length}`);
345
+ console.log(` Avg score: ${avgScore}`);
346
+ console.log(` Hot (8-10): ${hot}`);
347
+ console.log(` Warm (6-7): ${warm}`);
348
+ console.log(` Cool (4-5): ${cool}`);
349
+ console.log(` With email: ${leads.filter((l) => l.contact.email).length}`);
350
+ console.log(` With phone: ${leads.filter((l) => l.contact.phone).length}\n`);
351
+ }
352
+
353
+ main().catch((err) => {
354
+ console.error(`Fatal error: ${err.message}`);
355
+ exit(1);
356
+ });
@@ -0,0 +1,240 @@
1
+ ---
2
+ name: cashclaw-seo-auditor
3
+ description: Performs comprehensive SEO audits on websites covering technical SEO, on-page optimization, off-page signals, and performance metrics. Generates actionable reports with prioritized recommendations.
4
+ metadata:
5
+ {
6
+ "openclaw":
7
+ {
8
+ "emoji": "\U0001F50D",
9
+ "requires": { "bins": ["node", "curl"] },
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 SEO Auditor
25
+
26
+ You perform professional SEO audits that clients pay for. Your output must be
27
+ thorough, actionable, and formatted as a deliverable report. Never produce
28
+ generic advice -- every recommendation must reference specific findings from
29
+ the target URL.
30
+
31
+ ## Pricing Tiers
32
+
33
+ | Tier | Scope | Price | Delivery |
34
+ |------|-------|-------|----------|
35
+ | Basic | Single page, technical checks only | $9 | 1 hour |
36
+ | Standard | Up to 5 pages, full audit | $29 | 24 hours |
37
+ | Pro | Full site (up to 50 pages), competitive analysis | $59 | 48 hours |
38
+
39
+ ## Audit Process
40
+
41
+ ### Step 1: Fetch and Parse
42
+
43
+ Run the audit script to collect raw data:
44
+
45
+ ```bash
46
+ node scripts/audit.js --url "https://example.com" --output audit-data.json
47
+ ```
48
+
49
+ If the script is unavailable, perform manual checks using curl:
50
+
51
+ ```bash
52
+ # Fetch the page
53
+ curl -sL -o page.html -w "%{http_code} %{time_total}s %{size_download}bytes" "https://example.com"
54
+
55
+ # Check robots.txt
56
+ curl -sL "https://example.com/robots.txt"
57
+
58
+ # Check sitemap
59
+ curl -sL "https://example.com/sitemap.xml"
60
+
61
+ # Check SSL
62
+ curl -sI "https://example.com" | head -20
63
+
64
+ # Check redirect chain
65
+ curl -sIL "https://example.com" 2>&1 | grep -i "location\|HTTP/"
66
+ ```
67
+
68
+ ### Step 2: Technical SEO Checklist
69
+
70
+ Evaluate every item. Score each as PASS, WARN, or FAIL.
71
+
72
+ ```
73
+ [ ] HTTPS enabled and forced (HTTP redirects to HTTPS)
74
+ [ ] SSL certificate valid and not expiring within 30 days
75
+ [ ] robots.txt exists and is properly configured
76
+ [ ] XML sitemap exists and is valid
77
+ [ ] No broken internal links (4xx status codes)
78
+ [ ] No redirect chains longer than 2 hops
79
+ [ ] Clean URL structure (no query params for content pages)
80
+ [ ] Canonical tags present and correct
81
+ [ ] Hreflang tags (if multi-language)
82
+ [ ] Structured data / JSON-LD present and valid
83
+ [ ] Mobile-friendly viewport meta tag
84
+ [ ] No mixed content (HTTP resources on HTTPS page)
85
+ [ ] Server response time under 500ms
86
+ [ ] Gzip or Brotli compression enabled
87
+ [ ] HTTP/2 or HTTP/3 support
88
+ ```
89
+
90
+ ### Step 3: On-Page SEO Checklist
91
+
92
+ ```
93
+ [ ] Title tag exists, unique, 50-60 characters
94
+ [ ] Meta description exists, unique, 150-160 characters
95
+ [ ] H1 tag exists and is unique on the page
96
+ [ ] Heading hierarchy is logical (H1 > H2 > H3, no skips)
97
+ [ ] Images have descriptive alt text
98
+ [ ] Images are optimized (WebP/AVIF, lazy loading)
99
+ [ ] Internal links use descriptive anchor text
100
+ [ ] Content length adequate for topic (minimum 300 words for landing pages)
101
+ [ ] Keyword appears in: title, H1, first paragraph, URL
102
+ [ ] No duplicate content issues
103
+ [ ] No thin pages (< 100 words)
104
+ [ ] Open Graph tags present (og:title, og:description, og:image)
105
+ [ ] Twitter Card tags present
106
+ [ ] Favicon configured
107
+ ```
108
+
109
+ ### Step 4: Off-Page SEO Signals
110
+
111
+ For Standard and Pro tiers only:
112
+
113
+ ```
114
+ [ ] Check backlink profile (note: limited without API access, use available data)
115
+ [ ] Social media presence linked from site
116
+ [ ] Google Business Profile (if local business)
117
+ [ ] NAP consistency (Name, Address, Phone) across web
118
+ [ ] Domain authority estimation based on observable signals
119
+ [ ] Competitor comparison (Pro tier: audit top 3 competitors)
120
+ ```
121
+
122
+ ### Step 5: Performance Metrics
123
+
124
+ ```
125
+ [ ] Page load time (target: < 3 seconds)
126
+ [ ] First Contentful Paint (target: < 1.8s)
127
+ [ ] Largest Contentful Paint (target: < 2.5s)
128
+ [ ] Total page size (target: < 3MB)
129
+ [ ] Number of HTTP requests (target: < 50)
130
+ [ ] JavaScript bundle size (target: < 500KB compressed)
131
+ [ ] CSS optimization (unused CSS, render-blocking)
132
+ [ ] Font loading strategy (font-display: swap)
133
+ [ ] Third-party script impact
134
+ ```
135
+
136
+ ### Step 6: Score Calculation
137
+
138
+ Calculate an overall score from 0-100:
139
+
140
+ ```
141
+ Technical SEO: 35 points (each item = 35 / item_count)
142
+ On-Page SEO: 30 points (each item = 30 / item_count)
143
+ Off-Page SEO: 15 points (each item = 15 / item_count)
144
+ Performance: 20 points (each item = 20 / item_count)
145
+
146
+ PASS = full points
147
+ WARN = half points
148
+ FAIL = 0 points
149
+
150
+ Grade:
151
+ 90-100: A (Excellent)
152
+ 80-89: B (Good)
153
+ 70-79: C (Needs Improvement)
154
+ 60-69: D (Poor)
155
+ < 60: F (Critical)
156
+ ```
157
+
158
+ ## Report Format
159
+
160
+ Generate the final report in Markdown. This is what the client receives.
161
+
162
+ ```markdown
163
+ # SEO Audit Report
164
+ **URL:** {url}
165
+ **Date:** {date}
166
+ **Tier:** {Basic|Standard|Pro}
167
+ **Overall Score:** {score}/100 ({grade})
168
+
169
+ ---
170
+
171
+ ## Executive Summary
172
+ {2-3 sentences summarizing the site's SEO health and top priorities}
173
+
174
+ ## Score Breakdown
175
+ | Category | Score | Grade |
176
+ |----------|-------|-------|
177
+ | Technical SEO | {x}/35 | {grade} |
178
+ | On-Page SEO | {x}/30 | {grade} |
179
+ | Off-Page SEO | {x}/15 | {grade} |
180
+ | Performance | {x}/20 | {grade} |
181
+ | **Overall** | **{x}/100** | **{grade}** |
182
+
183
+ ## Critical Issues (Fix Immediately)
184
+ {Numbered list of FAIL items with specific fix instructions}
185
+
186
+ ## Warnings (Fix Soon)
187
+ {Numbered list of WARN items with recommendations}
188
+
189
+ ## What You Are Doing Well
190
+ {List of PASS items worth highlighting}
191
+
192
+ ## Priority Action Plan
193
+ 1. {Highest impact fix with estimated effort}
194
+ 2. {Second highest impact fix}
195
+ 3. {Third highest impact fix}
196
+ 4. ...
197
+
198
+ ## Detailed Findings
199
+
200
+ ### Technical SEO
201
+ {Table of all items with PASS/WARN/FAIL and notes}
202
+
203
+ ### On-Page SEO
204
+ {Table of all items with PASS/WARN/FAIL and notes}
205
+
206
+ ### Off-Page SEO (Standard/Pro only)
207
+ {Table of all items with PASS/WARN/FAIL and notes}
208
+
209
+ ### Performance
210
+ {Table of all items with PASS/WARN/FAIL and notes}
211
+
212
+ ---
213
+ *Generated by CashClaw SEO Auditor | cashclaw.ai*
214
+ ```
215
+
216
+ ## Deliverable Packaging
217
+
218
+ 1. Save the Markdown report as `seo-audit-{domain}-{date}.md`.
219
+ 2. Save the raw audit data as `seo-audit-{domain}-{date}.json`.
220
+ 3. If the client requests PDF, convert Markdown to PDF using available tools.
221
+ 4. Place all deliverables in the mission's `deliverables/` directory.
222
+ 5. Return to `cashclaw-core` with status `delivered` and file paths.
223
+
224
+ ## Quality Standards
225
+
226
+ - Every FAIL finding must include a specific, actionable fix (not "improve your SEO").
227
+ - Include code snippets where applicable (e.g., exact meta tag to add).
228
+ - Compare against industry benchmarks, not arbitrary standards.
229
+ - If data is unavailable for a check, mark as N/A and explain why.
230
+ - Pro tier must include at least 3 competitor comparisons.
231
+
232
+ ## Example curl to Test
233
+
234
+ ```bash
235
+ # Run a basic audit
236
+ cashclaw audit --url "https://example.com" --tier basic
237
+
238
+ # Run a standard audit
239
+ cashclaw audit --url "https://example.com" --tier standard --output report.md
240
+ ```