Files
scan-receipts/scripts/analyze_aso_keywords.mjs
Timo 84b9987c49 Add full application: receipt scanning, auth, billing, and account deletion
Brings the working codebase (Next.js app, auth system, Stripe billing,
Docker/deploy config, tests, docs) into version control on top of the
placeholder initial commit, and adds account self-deletion (Danger Zone
in Settings, password + typed-email confirmation, cascading DB cleanup,
Stripe cancellation) per GDPR right-to-erasure.

Excludes local build caches, node_modules, and internal agent scratch
files; .gitignore hardened to keep those out going forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 20:59:04 +02:00

121 lines
3.6 KiB
JavaScript

// Script to query Apple iTunes Search API for ASO & Competition Analysis
import https from 'https';
const KEYWORDS_DE = [
'receipt scanner',
'beleg scanner',
'belege digitalisieren',
'rechnung scanner',
'kassenbon scanner',
'quittung scanner',
'spesen app',
'buchhaltung scanner',
'datev scanner',
'receipt to excel',
'expense tracker',
'ausgaben tracker',
'belegmanager',
'rechnungsprogramm',
'fahrtenbuch und belege',
'steuer belege',
'ocr scanner excel'
];
const KEYWORDS_US = [
'receipt scanner',
'receipt to excel',
'receipt scanner to excel',
'expense tracker',
'receipt keeper',
'invoice scanner',
'receipts and expenses',
'ocr receipt scanner',
'bookkeeping scanner',
'mileage and receipts',
'tax receipt organizer',
'smart receipt'
];
function fetchAppleSearch(term, country = 'de', limit = 25) {
return new Promise((resolve, reject) => {
const url = `https://itunes.apple.com/search?term=${encodeURIComponent(term)}&country=${country}&entity=software&limit=${limit}`;
https.get(url, { headers: { 'User-Agent': 'Mozilla/5.0' } }, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
const json = JSON.parse(data);
resolve(json);
} catch (e) {
reject(e);
}
});
}).on('error', reject);
});
}
async function analyzeKeywords(keywords, country) {
console.log(`\n======================================================`);
console.log(`🔍 ANALYSING APPLE APP STORE SEARCH DATA [Country: ${country.toUpperCase()}]`);
console.log(`======================================================\n`);
const results = [];
for (const kw of keywords) {
try {
const data = await fetchAppleSearch(kw, country, 25);
const totalResults = data.resultCount;
const apps = data.results || [];
// Metrics calculation
const top5 = apps.slice(0, 5);
const top5Names = top5.map(a => a.trackName);
const avgRating = top5.reduce((acc, a) => acc + (a.averageUserRating || 0), 0) / (top5.length || 1);
const totalTop5Ratings = top5.reduce((acc, a) => acc + (a.userRatingCount || 0), 0);
const avgTop5RatingCount = Math.round(totalTop5Ratings / (top5.length || 1));
// Check title keyword match density in top 10
const titleMatches = apps.slice(0, 10).filter(a =>
(a.trackName || '').toLowerCase().includes(kw.toLowerCase()) ||
(a.description || '').toLowerCase().includes(kw.toLowerCase())
).length;
// Price distribution
const freeCount = top5.filter(a => a.price === 0).length;
results.push({
keyword: kw,
resultCount: totalResults,
top1: apps[0] ? `${apps[0].trackName} (${apps[0].userRatingCount || 0} reviews, ★${apps[0].averageUserRating?.toFixed(1) || '0'})` : 'None',
top5AvgRating: avgRating.toFixed(2),
avgTop5Reviews: avgTop5RatingCount,
titleMatchInTop10: titleMatches,
topCompetitors: top5.map(a => ({
name: a.trackName,
seller: a.sellerName,
reviews: a.userRatingCount || 0,
rating: a.averageUserRating || 0,
price: a.price,
genres: a.genres
}))
});
// Avoid hitting rate limits
await new Promise(r => setTimeout(r, 200));
} catch (err) {
console.error(`Error fetching "${kw}":`, err.message);
}
}
return results;
}
async function run() {
const deResults = await analyzeKeywords(KEYWORDS_DE, 'de');
const usResults = await analyzeKeywords(KEYWORDS_US, 'us');
console.log(JSON.stringify({ de: deResults, us: usResults }, null, 2));
}
run();