
Apify
- 124 installs
- 17.2k repo stars
- Updated August 1, 2026
- danielmiessler/personal_ai_infrastructure
Run Apify actors and datasets to scrape sites, extract structured records, and feed agents or pipelines with fresh web data for research and automation jobs.
About
apify from danielmiessler/personal_ai_infrastructure integrates the Apify scraping platform into personal AI infrastructure—launching actors, retrieving datasets, and normalizing web extracts—so agents and automations ingest live external data instead of static snapshots.
- Triggers Apify actors from agents
- Pulls structured scrape datasets
- Automates web data extraction jobs
- Feeds downstream analytics pipelines
- Handles pagination and site-specific actors
Apify by the numbers
- 124 all-time installs (skills.sh)
- Ranked #704 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/danielmiessler/personal_ai_infrastructure --skill apifyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 124 |
|---|---|
| repo stars | ★ 17.2k |
| Last updated | August 1, 2026 |
| Repository | danielmiessler/personal_ai_infrastructure ↗ |
What it does
Run Apify actors and datasets to scrape sites, extract structured records, and feed agents or pipelines with fresh web data for research and automation jobs.
Files
Customization
Before executing, check for user customizations at: ~/.claude/PAI/USER/SKILLCUSTOMIZATIONS/Apify/
If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults.
🚨 MANDATORY: Voice Notification (REQUIRED BEFORE ANY ACTION)
You MUST send this notification BEFORE doing anything else when this skill is invoked.
1. Send voice notification:
curl -s -X POST http://localhost:31337/notify \
-H "Content-Type: application/json" \
-d '{"message": "Running the WORKFLOWNAME workflow in the Apify skill to ACTION"}' \
> /dev/null 2>&1 &2. Output text notification:
Running the **WorkflowName** workflow in the **Apify** skill to ACTION...This is not optional. Execute this curl command immediately upon skill invocation.
Apify - Social Media & Web Scraping
Direct TypeScript access to 9 popular Apify actors with 99% token savings.
🔌 File-Based MCP
This skill is a file-based MCP - a code-first API wrapper that replaces token-heavy MCP protocol calls.
Why file-based? Filter data in code BEFORE returning to model context = 97.5% token savings.
🎯 Overview
Direct TypeScript access to the 9 most popular Apify actors without MCP overhead. Filter and transform data in code BEFORE it reaches the model context.
📊 Available Actors
Social Media (5 platforms)
- Instagram (145k users, 4.60★) - Profiles, posts, hashtags, comments
- LinkedIn (26k users, 4.10★) - Profiles, jobs, posts
- TikTok (90k users, 4.61★) - Profiles, videos, hashtags, comments
- YouTube (40k users, 4.40★) - Channels, videos, comments, search
- Facebook (35k users, 4.56★) - Posts, groups, comments
Business & Lead Generation
- Google Maps (198k users, 4.76★) - HIGHEST VALUE!
- Search businesses, extract contacts, reviews, images
- Perfect for lead generation
E-commerce
- Amazon (8k users, 4.97★) - Products, reviews, pricing
Web Scraping
- Web Scraper (94k users, 4.39★) - General-purpose, works with ANY website
🚀 Quick Start
Basic Usage Pattern
import { scrapeInstagramProfile, searchGoogleMaps } from 'actors'
// 1. Call the actor wrapper
const profile = await scrapeInstagramProfile({
username: 'target_username',
maxPosts: 50
})
// 2. Filter in code - BEFORE data reaches model!
const viral = profile.latestPosts?.filter(p => p.likesCount > 10000)
// 3. Only filtered results reach model context
console.log(viral) // ~10 posts instead of 50📚 Examples by Use Case
Social Media Monitoring
Instagram - Track engagement:
import { scrapeInstagramProfile, scrapeInstagramPosts } from 'actors'
// Get profile with recent posts
const profile = await scrapeInstagramProfile({
username: 'competitor',
maxPosts: 100
})
// Filter in code - only high-performing posts from last 30 days
const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000)
const topRecent = profile.latestPosts
?.filter(p =>
new Date(p.timestamp).getTime() > thirtyDaysAgo &&
p.likesCount > 5000
)
.sort((a, b) => b.likesCount - a.likesCount)
.slice(0, 10)
// Only 10 posts reach model instead of 100!LinkedIn - Job search:
import { searchLinkedInJobs } from 'actors'
const jobs = await searchLinkedInJobs({
keywords: 'AI engineer',
location: 'San Francisco',
remote: true,
maxResults: 200
})
// Filter in code - only senior roles at well-funded startups
const topJobs = jobs.filter(j =>
j.seniority?.includes('Senior') &&
parseInt(j.applicants || '0') > 50
)TikTok - Trend analysis:
import { scrapeTikTokHashtag } from 'actors'
const videos = await scrapeTikTokHashtag({
hashtag: 'ai',
maxResults: 500
})
// Filter in code - only viral content
const viral = videos
.filter(v => v.playCount > 1000000)
.sort((a, b) => b.playCount - a.playCount)
.slice(0, 20)Lead Generation (Business Intelligence)
Google Maps - Local business leads:
import { searchGoogleMaps } from 'actors'
// Search with contact info extraction
const places = await searchGoogleMaps({
query: 'restaurants in Austin',
maxResults: 500,
includeReviews: true,
maxReviewsPerPlace: 20,
scrapeContactInfo: true // Extracts emails from websites!
})
// Filter in code - only highly-rated with email/phone
const qualifiedLeads = places
.filter(p =>
p.rating >= 4.5 &&
p.reviewsCount >= 100 &&
(p.email || p.phone)
)
.map(p => ({
name: p.name,
rating: p.rating,
reviews: p.reviewsCount,
email: p.email,
phone: p.phone,
website: p.website,
address: p.address
}))
// Export leads - only qualified results!
console.log(`Found ${qualifiedLeads.length} qualified leads`)Google Maps - Review sentiment analysis:
import { scrapeGoogleMapsReviews } from 'actors'
const reviews = await scrapeGoogleMapsReviews({
placeUrl: 'https://maps.google.com/maps?cid=12345',
maxResults: 1000
})
// Filter in code - analyze sentiment by rating
const recentNegative = reviews
.filter(r => {
const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000)
return (
r.rating <= 2 &&
new Date(r.publishedAtDate).getTime() > thirtyDaysAgo &&
r.text.length > 50
)
})
// Identify common complaints
const complaints = recentNegative.map(r => r.text)E-commerce & Competitive Intelligence
Amazon - Price monitoring:
import { scrapeAmazonProduct } from 'actors'
const product = await scrapeAmazonProduct({
productUrl: 'https://www.amazon.com/dp/B08L5VT894',
includeReviews: true,
maxReviews: 200
})
// Filter in code - only recent negative reviews
const recentNegative = product.reviews
?.filter(r => {
const weekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000)
return (
r.rating <= 2 &&
new Date(r.date).getTime() > weekAgo
)
})
console.log(`Price: $${product.price}`)
console.log(`Rating: ${product.rating}/5`)
console.log(`Recent issues: ${recentNegative?.length} complaints`)Custom Web Scraping
Any Website - Custom extraction:
import { scrapeWebsite } from 'actors'
const products = await scrapeWebsite({
startUrls: ['https://example.com/products'],
linkSelector: 'a.product-link',
maxPagesPerCrawl: 100,
pageFunction: `
async function pageFunction(context) {
const { request, $, log } = context
return {
url: request.url,
title: $('h1.product-title').text(),
price: $('span.price').text(),
inStock: $('.in-stock').length > 0,
description: $('.description').text()
}
}
`
})
// Filter in code - only available products under $100
const affordable = products.filter(p =>
p.inStock &&
parseFloat(p.price.replace('$', '')) < 100
)🎨 Advanced Patterns
Pattern 1: Multi-Platform Social Listening
import {
scrapeInstagramHashtag,
scrapeTikTokHashtag,
searchYouTube
} from 'actors'
// Run all platforms in parallel
const [instagramPosts, tiktokVideos, youtubeVideos] = await Promise.all([
scrapeInstagramHashtag({ hashtag: 'ai', maxResults: 100 }),
scrapeTikTokHashtag({ hashtag: 'ai', maxResults: 100 }),
searchYouTube({ query: '#ai', maxResults: 100 })
])
// Combine and filter - only viral content across all platforms
const allViral = [
...instagramPosts.filter(p => p.likesCount > 10000),
...tiktokVideos.filter(v => v.playCount > 100000),
...youtubeVideos.filter(v => v.viewsCount > 50000)
]
console.log(`Found ${allViral.length} viral posts across 3 platforms`)Pattern 2: Lead Enrichment Pipeline
import { searchGoogleMaps, scrapeLinkedInProfile } from 'actors'
// 1. Find businesses on Google Maps
const restaurants = await searchGoogleMaps({
query: 'restaurants in SF',
maxResults: 100,
scrapeContactInfo: true
})
// 2. Filter for qualified leads
const qualified = restaurants.filter(r =>
r.rating >= 4.5 &&
r.email &&
r.reviewsCount >= 50
)
// 3. Enrich with LinkedIn data (if available)
const enriched = await Promise.all(
qualified.map(async (restaurant) => {
// Try to find LinkedIn company page
// ... additional enrichment logic
return restaurant
})
)Pattern 3: Competitive Analysis Dashboard
import {
scrapeInstagramProfile,
scrapeYouTubeChannel,
scrapeTikTokProfile
} from 'actors'
async function analyzeCompetitor(username: string) {
// Gather data from all platforms
const [instagram, youtube, tiktok] = await Promise.all([
scrapeInstagramProfile({ username, maxPosts: 30 }),
scrapeYouTubeChannel({ channelUrl: `https://youtube.com/@${username}`, maxVideos: 30 }),
scrapeTikTokProfile({ username, maxVideos: 30 })
])
// Calculate engagement metrics in code
return {
username,
instagram: {
followers: instagram.followersCount,
avgLikes: average(instagram.latestPosts?.map(p => p.likesCount) || []),
engagementRate: calculateEngagement(instagram)
},
youtube: {
subscribers: youtube.subscribersCount,
avgViews: average(youtube.videos?.map(v => v.viewsCount) || [])
},
tiktok: {
followers: tiktok.followersCount,
avgPlays: average(tiktok.videos?.map(v => v.playCount) || [])
}
}
}💰 Token Savings Calculator
Example: Instagram profile with 100 posts
MCP Approach:
1. search-actors → 1,000 tokens
2. call-actor → 1,000 tokens
3. get-actor-output → 50,000 tokens (100 unfiltered posts)
TOTAL: ~52,000 tokensFile-Based Approach:
const profile = await scrapeInstagramProfile({
username: 'user',
maxPosts: 100
})
// Filter in code - only top 10 posts
const top = profile.latestPosts
?.sort((a, b) => b.likesCount - a.likesCount)
.slice(0, 10)
// TOTAL: ~500 tokens (only 10 filtered posts reach model)Savings: 99% reduction (52,000 → 500 tokens)
🔧 Actor Reference
Social Media
scrapeInstagramProfile(input)- Profile + postsscrapeInstagramPosts(input)- Posts from userscrapeInstagramHashtag(input)- Posts by hashtagscrapeInstagramComments(input)- Comments on post
scrapeLinkedInProfile(input)- Profile + experience + emailsearchLinkedInJobs(input)- Job listingsscrapeLinkedInPosts(input)- Posts from profile/company
TikTok
scrapeTikTokProfile(input)- Profile + videosscrapeTikTokHashtag(input)- Videos by hashtagscrapeTikTokComments(input)- Comments on video
YouTube
scrapeYouTubeChannel(input)- Channel + videossearchYouTube(input)- Search videosscrapeYouTubeComments(input)- Comments on video
scrapeFacebookPosts(input)- Posts from pagesscrapeFacebookGroups(input)- Group postsscrapeFacebookComments(input)- Post comments
Business & Lead Generation
Google Maps
searchGoogleMaps(input)- Search places (with contact extraction!)scrapeGoogleMapsPlace(input)- Single place detailsscrapeGoogleMapsReviews(input)- Place reviews
E-commerce
Amazon
scrapeAmazonProduct(input)- Product details + reviewsscrapeAmazonReviews(input)- Product reviews only
Web Scraping
General Web
scrapeWebsite(input)- Custom multi-page crawlingscrapePage(url, pageFunction)- Single page extraction
⚙️ Configuration
Environment Variables:
# Required - Get from https://console.apify.com/account/integrations
APIFY_TOKEN=apify_api_xxxxx...Actor Run Options:
{
memory: 2048, // MB: 128, 256, 512, 1024, 2048, 4096, 8192
timeout: 300, // seconds
build: 'latest' // or specific build number
}🎯 When to Use This vs MCP
Use File-Based (this skill):
- ✅ Need to filter large datasets (>100 results)
- ✅ Want to transform/aggregate data in code
- ✅ Multiple sequential operations
- ✅ Control flow (loops, conditionals)
- ✅ Maximum token efficiency
Use MCP:
- ❌ Simple single operations with small results (<10 items)
- ❌ One-off exploratory queries
- ❌ Don't want to write code
🔗 Links
- Apify Platform: https://apify.com
- Actor Store: https://apify.com/store
- API Docs: https://docs.apify.com/api/v2
---
Remember: Filter data in code BEFORE returning to model context. This is where the 99% token savings happen!
Gotchas
- Actor selection matters. Each social platform has specific actors — don't use a generic scraper for Instagram when a dedicated Instagram actor exists.
- Rate limits vary by platform and plan. Check actor documentation for limits before running large scrapes.
- Scraped data format varies by actor. Read the actor's output schema before processing results.
Examples
Example 1: Scrape Instagram profile
User: "get the recent posts from this Instagram account"
→ Selects Instagram Profile actor
→ Runs with target profile URL
→ Returns structured post data (text, engagement, dates)Example 2: LinkedIn company scrape
User: "scrape this company's LinkedIn page"
→ Selects LinkedIn Company actor
→ Returns company info, employee count, recent postsExecution Log
After completing any workflow, append a single JSONL entry:
echo '{"ts":"'$(date -u +%Y-%m-%dT%H:%M:%SZ)'","skill":"Apify","workflow":"WORKFLOW_USED","input":"8_WORD_SUMMARY","status":"ok|error","duration_s":SECONDS}' >> ~/.claude/PAI/MEMORY/SKILLS/execution.jsonlReplace WORKFLOW_USED with the workflow executed, 8_WORD_SUMMARY with a brief input description, and SECONDS with approximate wall-clock time. Log status: "error" if the workflow failed.
node_modules/
bun.lock
*.log
.DS_Store
/**
* Google Maps Scraper
*
* Apify Actor: compass/crawler-google-places (198,093 users, 4.76 rating)
* Pricing: $0.001-$0.007 per event (Actor start + per place + optional add-ons)
*
* HIGHEST VALUE ACTOR - 198k users!
* Extract Google Maps business data, reviews, contacts, images - perfect for lead generation.
*/
import { Apify } from '../../index'
import type {
BusinessInfo,
Location,
ContactInfo,
PaginationOptions,
ActorRunOptions
} from '../../types'
/* ============================================================================
* TYPES
* ========================================================================= */
export interface GoogleMapsSearchInput extends PaginationOptions {
/** Search query (e.g., "restaurants in San Francisco") */
query: string
/** Maximum number of places to scrape */
maxResults?: number
/** Include reviews for each place */
includeReviews?: boolean
/** Maximum reviews per place */
maxReviewsPerPlace?: number
/** Include images */
includeImages?: boolean
/** Scrape contact information from websites */
scrapeContactInfo?: boolean
/** Language code (en, es, fr, de, etc.) */
language?: string
/** Country code for search region */
country?: string
}
export interface GoogleMapsPlaceInput {
/** Google Maps place URL or Place ID */
placeUrl: string
/** Include reviews */
includeReviews?: boolean
/** Maximum reviews to scrape */
maxReviews?: number
/** Include images */
includeImages?: boolean
/** Scrape contact info from website */
scrapeContactInfo?: boolean
}
export interface GoogleMapsPlace extends BusinessInfo {
placeId: string
name: string
url: string
category?: string
categories?: string[]
address?: string
location?: Location
rating?: number
reviewsCount?: number
priceLevel?: number
phone?: string
website?: string
email?: string
openingHours?: OpeningHours
popularTimes?: PopularTimes[]
isTemporarilyClosed?: boolean
isPermanentlyClosed?: boolean
totalScore?: number
reviewsDistribution?: ReviewsDistribution
imageUrls?: string[]
reviews?: GoogleMapsReview[]
contactInfo?: ContactInfo
socialMedia?: {
facebook?: string
twitter?: string
instagram?: string
linkedin?: string
}
verificationStatus?: string
}
export interface OpeningHours {
monday?: string
tuesday?: string
wednesday?: string
thursday?: string
friday?: string
saturday?: string
sunday?: string
}
export interface PopularTimes {
day: string
hours: Array<{
hour: number
occupancyPercent: number
}>
}
export interface ReviewsDistribution {
oneStar?: number
twoStar?: number
threeStar?: number
fourStar?: number
fiveStar?: number
}
export interface GoogleMapsReview {
id?: string
text: string
publishedAtDate: string
rating: number
likesCount?: number
reviewerId?: string
reviewerName?: string
reviewerPhotoUrl?: string
reviewerReviewsCount?: number
responseFromOwner?: string
responseFromOwnerDate?: string
imageUrls?: string[]
}
export interface GoogleMapsReviewsInput extends PaginationOptions {
/** Google Maps place URL */
placeUrl: string
/** Maximum number of reviews to scrape */
maxResults?: number
/** Minimum rating filter (1-5) */
minRating?: number
/** Language code */
language?: string
}
/* ============================================================================
* FUNCTIONS
* ========================================================================= */
/**
* Search Google Maps for places matching a query
*
* @param input - Search parameters
* @param options - Actor run options
* @returns Array of Google Maps places
*
* @example
* ```typescript
* // Search for coffee shops in SF
* const places = await searchGoogleMaps({
* query: 'coffee shops in San Francisco',
* maxResults: 50,
* includeReviews: true,
* maxReviewsPerPlace: 10
* })
*
* // Filter in code - only highly rated with many reviews
* const topCoffeeShops = places
* .filter(p => p.rating >= 4.5 && p.reviewsCount >= 100)
* .sort((a, b) => b.rating - a.rating)
* .slice(0, 10)
*
* // Extract emails for lead generation
* const leads = topCoffeeShops
* .filter(p => p.email)
* .map(p => ({ name: p.name, email: p.email, phone: p.phone }))
* ```
*/
export async function searchGoogleMaps(
input: GoogleMapsSearchInput,
options?: ActorRunOptions
): Promise<GoogleMapsPlace[]> {
const apify = new Apify()
const run = await apify.callActor('compass/crawler-google-places', {
searchStringsArray: [input.query],
maxCrawledPlacesPerSearch: input.maxResults || 50,
language: input.language || 'en',
countryCode: input.country,
includeReviews: input.includeReviews || false,
maxReviews: input.maxReviewsPerPlace || 0,
includeImages: input.includeImages || false,
scrapeCompanyEmails: input.scrapeContactInfo || false,
scrapeSocialMediaLinks: input.scrapeContactInfo || false
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`Google Maps search failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({
limit: input.maxResults || 1000,
offset: input.offset || 0
})
return items.map(transformPlace)
}
/**
* Scrape detailed data for a specific Google Maps place
*
* @param input - Place scraping parameters
* @param options - Actor run options
* @returns Detailed place information
*
* @example
* ```typescript
* // Scrape a specific place with reviews
* const place = await scrapeGoogleMapsPlace({
* placeUrl: 'https://maps.google.com/maps?cid=12345',
* includeReviews: true,
* maxReviews: 100,
* scrapeContactInfo: true
* })
*
* // Filter reviews in code - only recent 5-star reviews
* const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000)
* const recentExcellent = place.reviews?.filter(r =>
* r.rating === 5 &&
* new Date(r.publishedAtDate).getTime() > thirtyDaysAgo
* )
* ```
*/
export async function scrapeGoogleMapsPlace(
input: GoogleMapsPlaceInput,
options?: ActorRunOptions
): Promise<GoogleMapsPlace> {
const apify = new Apify()
const run = await apify.callActor('compass/crawler-google-places', {
startUrls: [input.placeUrl],
includeReviews: input.includeReviews || false,
maxReviews: input.maxReviews || 0,
includeImages: input.includeImages || false,
scrapeCompanyEmails: input.scrapeContactInfo || false,
scrapeSocialMediaLinks: input.scrapeContactInfo || false
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`Google Maps place scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({ limit: 1 })
if (items.length === 0) {
throw new Error(`Place not found: ${input.placeUrl}`)
}
return transformPlace(items[0])
}
/**
* Scrape reviews for a Google Maps place
*
* @param input - Review scraping parameters
* @param options - Actor run options
* @returns Array of reviews
*
* @example
* ```typescript
* // Get 500 reviews for sentiment analysis
* const reviews = await scrapeGoogleMapsReviews({
* placeUrl: 'https://maps.google.com/maps?cid=12345',
* maxResults: 500,
* language: 'en'
* })
*
* // Filter in code - only detailed reviews
* const detailedReviews = reviews.filter(r =>
* r.text.length > 100 &&
* r.imageUrls && r.imageUrls.length > 0
* )
*
* // Analyze sentiment by rating
* const negative = reviews.filter(r => r.rating <= 2)
* const positive = reviews.filter(r => r.rating >= 4)
* ```
*/
export async function scrapeGoogleMapsReviews(
input: GoogleMapsReviewsInput,
options?: ActorRunOptions
): Promise<GoogleMapsReview[]> {
const apify = new Apify()
const run = await apify.callActor('compass/Google-Maps-Reviews-Scraper', {
startUrls: [input.placeUrl],
maxReviews: input.maxResults || 100,
reviewsSort: 'newest',
language: input.language || 'en'
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`Google Maps reviews scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({
limit: input.maxResults || 1000,
offset: input.offset || 0
})
// Filter by rating if specified
let reviews = items.map(transformReview)
if (input.minRating) {
reviews = reviews.filter(r => r.rating >= input.minRating!)
}
return reviews
}
/* ============================================================================
* HELPERS
* ========================================================================= */
/**
* Transform raw Google Maps place data to our standard format
*/
function transformPlace(place: any): GoogleMapsPlace {
return {
placeId: place.placeId,
name: place.title || place.name,
url: place.url,
category: place.categoryName,
categories: place.categories || [place.categoryName],
address: place.address,
location: {
latitude: place.location?.lat,
longitude: place.location?.lng,
address: place.address,
city: place.city,
state: place.state,
country: place.countryCode,
postalCode: place.postalCode
},
rating: place.totalScore,
totalScore: place.totalScore,
reviewsCount: place.reviewsCount,
priceLevel: place.priceLevel,
phone: place.phone,
website: place.website,
email: place.email || place.companyEmail,
openingHours: place.openingHours,
popularTimes: place.popularTimesHistogram,
isTemporarilyClosed: place.temporarilyClosed,
isPermanentlyClosed: place.permanentlyClosed,
reviewsDistribution: place.reviewsDistribution,
imageUrls: place.imageUrls,
reviews: place.reviews?.map(transformReview),
contact: {
email: place.email || place.companyEmail,
phone: place.phone,
website: place.website,
socialMedia: {
facebook: place.facebookUrl,
twitter: place.twitterUrl,
instagram: place.instagramUrl,
linkedin: place.linkedinUrl
}
},
contactInfo: {
email: place.email || place.companyEmail,
phone: place.phone,
website: place.website
},
socialMedia: {
facebook: place.facebookUrl,
twitter: place.twitterUrl,
instagram: place.instagramUrl,
linkedin: place.linkedinUrl
},
verificationStatus: place.claimThisBusiness
}
}
/**
* Transform raw Google Maps review data to our standard format
*/
function transformReview(review: any): GoogleMapsReview {
return {
id: review.reviewId,
text: review.text || review.reviewText,
publishedAtDate: review.publishedAtDate || review.publishAt,
rating: review.stars || review.rating,
likesCount: review.likesCount,
reviewerId: review.reviewerId,
reviewerName: review.name || review.reviewerName,
reviewerPhotoUrl: review.profilePhotoUrl || review.reviewerPhotoUrl,
reviewerReviewsCount: review.reviewerNumberOfReviews,
responseFromOwner: review.responseFromOwnerText,
responseFromOwnerDate: review.responseFromOwnerDate,
imageUrls: review.reviewImageUrls
}
}
/**
* Business & Lead Generation Actors
*
* - Google Maps (198k users - HIGHEST VALUE!)
*/
export * from './google-maps'
/**
* Amazon Scraper
*
* Top Actors:
* - junglee/free-amazon-product-scraper (8,898 users, 4.97 rating)
* - axesso_data/amazon-reviews-scraper (1,647 users, 4.62 rating, $0.75/1k reviews)
*
* Extract Amazon product data, reviews, pricing without API.
*/
import { Apify } from '../../index'
import type {
PaginationOptions,
ActorRunOptions
} from '../../types'
/* ============================================================================
* TYPES
* ========================================================================= */
export interface AmazonProductInput {
/** Amazon product URL or ASIN */
productUrl: string
/** Include reviews */
includeReviews?: boolean
/** Maximum reviews to scrape */
maxReviews?: number
}
export interface AmazonProduct {
asin: string
title: string
url: string
price?: number
currency?: string
priceString?: string
originalPrice?: number
discount?: string
rating?: number
reviewsCount?: number
stars?: number
description?: string
features?: string[]
images?: string[]
variants?: ProductVariant[]
availability?: string
inStock?: boolean
seller?: string
brand?: string
category?: string
reviews?: AmazonReview[]
}
export interface ProductVariant {
asin: string
title: string
price?: number
imageUrl?: string
}
export interface AmazonReviewsInput extends PaginationOptions {
/** Amazon product URL or ASIN */
productUrl: string
/** Maximum reviews to scrape */
maxResults?: number
/** Star rating filter (1-5) */
starRating?: number
/** Verified purchases only */
verifiedOnly?: boolean
}
export interface AmazonReview {
id: string
title: string
text: string
rating: number
date: string
verifiedPurchase?: boolean
helpful?: number
reviewerName?: string
reviewerUrl?: string
images?: string[]
}
/* ============================================================================
* FUNCTIONS
* ========================================================================= */
/**
* Scrape Amazon product data
*
* @param input - Product scraping options
* @param options - Actor run options
* @returns Amazon product details
*
* @example
* ```typescript
* const product = await scrapeAmazonProduct({
* productUrl: 'https://www.amazon.com/dp/B08L5VT894',
* includeReviews: true,
* maxReviews: 50
* })
*
* console.log(`${product.title} - $${product.price}`)
* console.log(`Rating: ${product.rating}/5 (${product.reviewsCount} reviews)`)
*
* // Filter reviews in code - only 5-star verified purchases
* const topReviews = product.reviews?.filter(r =>
* r.rating === 5 && r.verifiedPurchase
* )
* ```
*/
export async function scrapeAmazonProduct(
input: AmazonProductInput,
options?: ActorRunOptions
): Promise<AmazonProduct> {
const apify = new Apify()
const run = await apify.callActor('junglee/free-amazon-product-scraper', {
startUrls: [input.productUrl],
maxReviews: input.maxReviews || 0,
includeReviews: input.includeReviews || false
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`Amazon product scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({ limit: 1 })
if (items.length === 0) {
throw new Error(`Product not found: ${input.productUrl}`)
}
const product = items[0]
return {
asin: product.asin,
title: product.title,
url: product.url || input.productUrl,
price: product.price,
currency: product.currency,
priceString: product.priceString,
originalPrice: product.originalPrice,
discount: product.discount,
rating: product.stars || product.rating,
stars: product.stars,
reviewsCount: product.reviews || product.reviewsCount,
description: product.description,
features: product.features || product.featureBullets,
images: product.images,
variants: product.variants,
availability: product.availability,
inStock: product.inStock,
seller: product.seller,
brand: product.brand,
category: product.category,
reviews: product.topReviews?.map((r: any) => ({
id: r.id,
title: r.title,
text: r.text || r.body,
rating: r.stars || r.rating,
date: r.date,
verifiedPurchase: r.verified,
helpful: r.helpful,
reviewerName: r.reviewer,
reviewerUrl: r.reviewerUrl,
images: r.images
}))
}
}
/**
* Scrape Amazon product reviews
*
* @param input - Review scraping options
* @param options - Actor run options
* @returns Array of Amazon reviews
*
* @example
* ```typescript
* const reviews = await scrapeAmazonReviews({
* productUrl: 'https://www.amazon.com/dp/B08L5VT894',
* maxResults: 500,
* verifiedOnly: true
* })
*
* // Filter in code - only detailed reviews
* const detailed = reviews.filter(r =>
* r.text.length > 200 &&
* r.images && r.images.length > 0
* )
*
* // Analyze sentiment by star rating
* const positive = reviews.filter(r => r.rating >= 4)
* const negative = reviews.filter(r => r.rating <= 2)
* console.log(`Sentiment: ${positive.length}+ / ${negative.length}-`)
* ```
*/
export async function scrapeAmazonReviews(
input: AmazonReviewsInput,
options?: ActorRunOptions
): Promise<AmazonReview[]> {
const apify = new Apify()
const run = await apify.callActor('axesso_data/amazon-reviews-scraper', {
urls: [input.productUrl],
maxReviews: input.maxResults || 100,
starRating: input.starRating,
verifiedPurchaseOnly: input.verifiedOnly
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`Amazon reviews scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({
limit: input.maxResults || 1000,
offset: input.offset || 0
})
return items.map((review: any) => ({
id: review.id || review.reviewId,
title: review.title,
text: review.text || review.body,
rating: review.stars || review.rating,
date: review.date,
verifiedPurchase: review.verifiedPurchase || review.verified,
helpful: review.helpful || review.helpfulCount,
reviewerName: review.reviewerName || review.author,
reviewerUrl: review.reviewerUrl,
images: review.images || review.reviewImages
}))
}
/**
* E-commerce Actors
*
* - Amazon (products, reviews, pricing)
*/
export * from './amazon'
/**
* Apify Actors - File-Based API Wrappers
*
* Direct API access to the most popular Apify actors without MCP overhead.
* Filter data in code BEFORE returning to model context for massive token savings.
*
* Categories:
* - Social Media: Instagram, LinkedIn, TikTok, YouTube, Facebook
* - Business: Google Maps (lead generation)
* - E-commerce: Amazon
* - Web: General-purpose web scraper
*
* Token Efficiency Example:
* - MCP approach: ~50,000 tokens (full unfiltered dataset)
* - Code-first approach: ~500 tokens (filtered top 10 results)
* - Savings: 99% token reduction!
*
* @example
* ```typescript
* import { scrapeInstagramProfile, searchGoogleMaps } from ''
*
* // Instagram profile with filtering
* const profile = await scrapeInstagramProfile({
* username: 'exampleuser',
* maxPosts: 50
* })
*
* // Filter in code - only viral posts
* const viral = profile.latestPosts?.filter(p => p.likesCount > 10000)
*
* // Google Maps lead generation
* const places = await searchGoogleMaps({
* query: 'coffee shops in San Francisco',
* maxResults: 100,
* scrapeContactInfo: true
* })
*
* // Filter in code - only highly rated with email
* const leads = places
* .filter(p => p.rating >= 4.5 && p.email)
* .map(p => ({ name: p.name, email: p.email, phone: p.phone }))
* ```
*/
// Social Media
export * from './social-media'
// Business & Lead Generation
export * from './business'
// E-commerce
export * from './ecommerce'
// Web Scraping
export * from './web'
/**
* Facebook Scraper
*
* Top Actors:
* - apify/facebook-posts-scraper (35,226 users, 4.56 rating)
* - apify/facebook-groups-scraper (16,182 users, 4.19 rating)
* - apify/facebook-comments-scraper (17,173 users, 4.46 rating)
*
* Extract Facebook posts, groups, comments, pages without login.
*/
import { Apify } from '../../index'
import type {
Post,
UserProfile,
PaginationOptions,
ActorRunOptions
} from '../../types'
/* ============================================================================
* TYPES
* ========================================================================= */
export interface FacebookPostsInput extends PaginationOptions {
/** Facebook page or profile URLs */
pageUrls: string[]
/** Maximum number of posts per page */
maxPostsPerPage?: number
/** Date range filter */
fromDate?: string
toDate?: string
}
export interface FacebookPost extends Post {
id: string
url: string
text?: string
postDate: string
pageUrl?: string
pageName?: string
likesCount?: number
commentsCount?: number
sharesCount?: number
imageUrls?: string[]
videoUrl?: string
type?: 'post' | 'video' | 'image' | 'link'
}
export interface FacebookGroupsInput extends PaginationOptions {
/** Facebook group URLs */
groupUrls: string[]
/** Maximum posts per group */
maxPostsPerGroup?: number
/** Include comments */
includeComments?: boolean
}
export interface FacebookGroupPost extends FacebookPost {
groupName?: string
groupUrl?: string
authorName?: string
authorUrl?: string
comments?: FacebookComment[]
}
export interface FacebookCommentsInput extends PaginationOptions {
/** Facebook post URLs */
postUrls: string[]
/** Maximum comments per post */
maxCommentsPerPost?: number
}
export interface FacebookComment {
id: string
text: string
date: string
likesCount?: number
authorName?: string
authorUrl?: string
}
/* ============================================================================
* FUNCTIONS
* ========================================================================= */
/**
* Scrape Facebook posts from pages or profiles
*
* @param input - Posts scraping options
* @param options - Actor run options
* @returns Array of Facebook posts
*
* @example
* ```typescript
* const posts = await scrapeFacebookPosts({
* pageUrls: ['https://www.facebook.com/SomePage'],
* maxPostsPerPage: 100
* })
*
* // Filter in code - only high-engagement posts
* const viral = posts.filter(p =>
* p.likesCount > 1000 || p.sharesCount > 100
* )
* ```
*/
export async function scrapeFacebookPosts(
input: FacebookPostsInput,
options?: ActorRunOptions
): Promise<FacebookPost[]> {
const apify = new Apify()
const run = await apify.callActor('apify/facebook-posts-scraper', {
startUrls: input.pageUrls.map(url => ({ url })),
maxPosts: input.maxPostsPerPage || 50,
fromDate: input.fromDate,
toDate: input.toDate
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`Facebook posts scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({
limit: input.maxResults || 1000,
offset: input.offset || 0
})
return items.map((post: any) => ({
id: post.id,
url: post.url,
text: post.text,
caption: post.text,
postDate: post.time,
timestamp: post.time,
pageUrl: post.pageUrl,
pageName: post.pageName,
likesCount: post.likes,
commentsCount: post.comments,
sharesCount: post.shares,
imageUrls: post.images,
videoUrl: post.video,
type: post.postType
}))
}
/**
* Scrape Facebook groups posts
*
* @param input - Groups scraping options
* @param options - Actor run options
* @returns Array of group posts
*
* @example
* ```typescript
* const posts = await scrapeFacebookGroups({
* groupUrls: ['https://www.facebook.com/groups/somegroupid'],
* maxPostsPerGroup: 50,
* includeComments: true
* })
*
* // Filter in code - only posts with active discussion
* const activeDiscussions = posts.filter(p =>
* p.commentsCount > 10
* )
* ```
*/
export async function scrapeFacebookGroups(
input: FacebookGroupsInput,
options?: ActorRunOptions
): Promise<FacebookGroupPost[]> {
const apify = new Apify()
const run = await apify.callActor('apify/facebook-groups-scraper', {
startUrls: input.groupUrls.map(url => ({ url })),
maxPosts: input.maxPostsPerGroup || 50,
includeComments: input.includeComments || false
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`Facebook groups scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({
limit: input.maxResults || 1000,
offset: input.offset || 0
})
return items.map((post: any) => ({
id: post.id,
url: post.url,
text: post.text,
caption: post.text,
postDate: post.time,
timestamp: post.time,
groupName: post.groupName,
groupUrl: post.groupUrl,
authorName: post.authorName,
authorUrl: post.authorUrl,
likesCount: post.likes,
commentsCount: post.comments,
sharesCount: post.shares,
imageUrls: post.images,
videoUrl: post.video,
comments: post.comments?.map((c: any) => ({
id: c.id,
text: c.text,
date: c.time,
likesCount: c.likes,
authorName: c.authorName,
authorUrl: c.authorUrl
}))
}))
}
/**
* Scrape Facebook comments from posts
*
* @param input - Comments scraping options
* @param options - Actor run options
* @returns Array of comments
*
* @example
* ```typescript
* const comments = await scrapeFacebookComments({
* postUrls: ['https://www.facebook.com/post/123'],
* maxCommentsPerPost: 200
* })
*
* // Filter in code - only highly-liked comments
* const topComments = comments
* .filter(c => c.likesCount > 50)
* .sort((a, b) => b.likesCount - a.likesCount)
* ```
*/
export async function scrapeFacebookComments(
input: FacebookCommentsInput,
options?: ActorRunOptions
): Promise<FacebookComment[]> {
const apify = new Apify()
const run = await apify.callActor('apify/facebook-comments-scraper', {
startUrls: input.postUrls.map(url => ({ url })),
maxComments: input.maxCommentsPerPost || 100
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`Facebook comments scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({
limit: input.maxResults || 1000,
offset: input.offset || 0
})
return items.map((comment: any) => ({
id: comment.id,
text: comment.text,
date: comment.time,
likesCount: comment.likes,
authorName: comment.authorName,
authorUrl: comment.authorUrl
}))
}
/**
* Social Media Actors
*
* Comprehensive social media scraping capabilities:
* - Instagram (145k users)
* - LinkedIn (26k users)
* - TikTok (90k users)
* - YouTube (40k users)
* - Facebook (35k users)
* - Twitter/X (Unlimited)
*/
export * from './instagram'
export * from './linkedin'
export * from './tiktok'
export * from './youtube'
export * from './facebook'
export * from './twitter'
/**
* Instagram Scraper
*
* Apify Actor: apify/instagram-scraper (145,279 users, 4.60 rating)
* Pricing: $0.50-$2.70 per 1000 results (tiered)
*
* Extract Instagram profiles, posts, hashtags, comments without login.
*/
import { Apify } from '../../index'
import type {
UserProfile,
Post,
EngagementMetrics,
PaginationOptions,
ActorRunOptions
} from '../../types'
/* ============================================================================
* TYPES
* ========================================================================= */
export interface InstagramProfileInput {
/** Instagram username (without @) */
username: string
/** Maximum number of latest posts to include */
maxPosts?: number
/** Include profile metadata */
includeMetadata?: boolean
}
export interface InstagramProfile extends UserProfile {
username: string
fullName: string
biography?: string
externalUrl?: string
followersCount: number
followingCount: number
postsCount: number
isPrivate?: boolean
isVerified?: boolean
latestPosts?: InstagramPost[]
}
export interface InstagramPost extends Post {
id: string
shortCode: string
url: string
caption?: string
imageUrl?: string
videoUrl?: string
likesCount: number
commentsCount: number
timestamp: string
type: 'Image' | 'Video' | 'Sidecar'
location?: {
name?: string
slug?: string
}
hashtags?: string[]
mentions?: string[]
isSponsored?: boolean
}
export interface InstagramPostsInput extends PaginationOptions {
/** Instagram username (without @) */
username: string
/** Maximum number of posts to scrape */
maxResults?: number
}
export interface InstagramHashtagInput extends PaginationOptions {
/** Hashtag (without #) */
hashtag: string
/** Maximum number of posts to scrape */
maxResults?: number
}
export interface InstagramCommentInput extends PaginationOptions {
/** Instagram post URL */
postUrl: string
/** Maximum number of comments to scrape */
maxResults?: number
}
export interface InstagramComment {
id: string
text: string
timestamp: string
likesCount: number
ownerUsername: string
ownerProfilePicUrl?: string
}
/* ============================================================================
* FUNCTIONS
* ========================================================================= */
/**
* Scrape Instagram profile data
*
* @param input - Profile scraping options
* @param options - Actor run options (memory, timeout)
* @returns Profile data with optional latest posts
*
* @example
* ```typescript
* // Get profile with latest 12 posts
* const profile = await scrapeInstagramProfile({
* username: 'exampleuser',
* maxPosts: 12
* })
*
* // Filter in code - only high-engagement posts
* const viralPosts = profile.latestPosts?.filter(p => p.likesCount > 10000)
* console.log(`Found ${viralPosts?.length} viral posts`)
* ```
*/
export async function scrapeInstagramProfile(
input: InstagramProfileInput,
options?: ActorRunOptions
): Promise<InstagramProfile> {
const apify = new Apify()
// Call the Instagram Profile Scraper actor
const run = await apify.callActor('apify/instagram-profile-scraper', {
usernames: [input.username],
resultsLimit: input.maxPosts || 12
}, options)
// Wait for completion
await apify.waitForRun(run.id)
// Check status
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`Instagram profile scraping failed: ${finalRun.status}`)
}
// Get results
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({ limit: 1 })
if (items.length === 0) {
throw new Error(`Profile not found: @${input.username}`)
}
const profile = items[0]
// Transform to our interface
return {
id: profile.id,
username: profile.username,
fullName: profile.fullName || profile.username,
biography: profile.biography,
externalUrl: profile.externalUrl,
profilePictureUrl: profile.profilePicUrl,
followersCount: profile.followersCount || 0,
followingCount: profile.followsCount || 0,
postsCount: profile.postsCount || 0,
isPrivate: profile.private,
isVerified: profile.verified,
verified: profile.verified,
latestPosts: profile.latestPosts?.map((post: any) => transformPost(post))
}
}
/**
* Scrape Instagram posts from a profile
*
* @param input - Posts scraping options
* @param options - Actor run options
* @returns Array of Instagram posts
*
* @example
* ```typescript
* // Scrape latest 50 posts
* const posts = await scrapeInstagramPosts({
* username: 'exampleuser',
* maxResults: 50
* })
*
* // Filter in code - posts from last 30 days with high engagement
* const thirtyDaysAgo = Date.now() - (30 * 24 * 60 * 60 * 1000)
* const recentPopular = posts.filter(p =>
* new Date(p.timestamp).getTime() > thirtyDaysAgo &&
* p.likesCount > 1000
* )
*
* // Only filtered results reach model context!
* console.log(recentPopular)
* ```
*/
export async function scrapeInstagramPosts(
input: InstagramPostsInput,
options?: ActorRunOptions
): Promise<InstagramPost[]> {
const apify = new Apify()
// Use the main Instagram scraper for posts
const run = await apify.callActor('apify/instagram-post-scraper', {
usernames: [input.username],
resultsLimit: input.maxResults || 50
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`Instagram posts scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({
limit: input.maxResults || 1000,
offset: input.offset || 0
})
return items.map(transformPost)
}
/**
* Scrape Instagram posts by hashtag
*
* @param input - Hashtag scraping options
* @param options - Actor run options
* @returns Array of Instagram posts with that hashtag
*
* @example
* ```typescript
* // Scrape top 100 posts for a hashtag
* const posts = await scrapeInstagramHashtag({
* hashtag: 'ai',
* maxResults: 100
* })
*
* // Filter in code - only videos with high views
* const popularVideos = posts.filter(p =>
* p.type === 'Video' &&
* p.likesCount > 5000
* ).slice(0, 10)
* ```
*/
export async function scrapeInstagramHashtag(
input: InstagramHashtagInput,
options?: ActorRunOptions
): Promise<InstagramPost[]> {
const apify = new Apify()
const run = await apify.callActor('apify/instagram-hashtag-scraper', {
hashtags: [input.hashtag],
resultsLimit: input.maxResults || 100
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`Instagram hashtag scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({
limit: input.maxResults || 1000,
offset: input.offset || 0
})
return items.map(transformPost)
}
/**
* Scrape Instagram comments from a post
*
* @param input - Comment scraping options
* @param options - Actor run options
* @returns Array of comments
*
* @example
* ```typescript
* const comments = await scrapeInstagramComments({
* postUrl: 'https://www.instagram.com/p/ABC123/',
* maxResults: 100
* })
*
* // Filter in code - only comments with likes
* const popularComments = comments
* .filter(c => c.likesCount > 10)
* .sort((a, b) => b.likesCount - a.likesCount)
* .slice(0, 10)
* ```
*/
export async function scrapeInstagramComments(
input: InstagramCommentInput,
options?: ActorRunOptions
): Promise<InstagramComment[]> {
const apify = new Apify()
const run = await apify.callActor('apify/instagram-comment-scraper', {
directUrls: [input.postUrl],
resultsLimit: input.maxResults || 100
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`Instagram comments scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({
limit: input.maxResults || 1000,
offset: input.offset || 0
})
return items.map((item: any) => ({
id: item.id,
text: item.text,
timestamp: item.timestamp,
likesCount: item.likesCount || 0,
ownerUsername: item.ownerUsername,
ownerProfilePicUrl: item.ownerProfilePicUrl
}))
}
/* ============================================================================
* HELPERS
* ========================================================================= */
/**
* Transform raw Instagram post data to our standard format
*/
function transformPost(post: any): InstagramPost {
return {
id: post.id,
shortCode: post.shortCode,
url: post.url || `https://www.instagram.com/p/${post.shortCode}/`,
caption: post.caption,
imageUrl: post.displayUrl || post.imageUrl,
videoUrl: post.videoUrl,
likesCount: post.likesCount || 0,
commentsCount: post.commentsCount || 0,
viewsCount: post.videoViewCount,
timestamp: post.timestamp,
type: post.type || (post.videoUrl ? 'Video' : 'Image'),
location: post.locationName ? {
name: post.locationName,
slug: post.locationSlug
} : undefined,
hashtags: post.hashtags,
mentions: post.mentions,
isSponsored: post.isSponsored,
text: post.caption,
author: post.ownerUsername ? {
username: post.ownerUsername,
fullName: post.ownerFullName
} : undefined
}
}
/**
* LinkedIn Scraper
*
* Top Actors:
* - dev_fusion/Linkedin-Profile-Scraper (26,635 users, 4.10 rating, $10/1k results)
* - curious_coder/linkedin-jobs-scraper (9,430 users, 4.98 rating, $1/1k results)
* - supreme_coder/linkedin-post (3,663 users, 4.16 rating, $0.001/post)
*
* Extract LinkedIn profiles, jobs, posts, company data without cookies.
*/
import { Apify } from '../../index'
import type {
UserProfile,
Post,
PaginationOptions,
ActorRunOptions,
Location,
ContactInfo
} from '../../types'
/* ============================================================================
* TYPES
* ========================================================================= */
export interface LinkedInProfileInput {
/** LinkedIn profile URL */
profileUrl: string
/** Include email extraction (requires website visit) */
includeEmail?: boolean
}
export interface LinkedInProfile extends UserProfile {
fullName: string
headline?: string
location?: string
about?: string
profileUrl: string
company?: string
position?: string
email?: string
phone?: string
website?: string
connectionsCount?: number
skills?: string[]
experience?: LinkedInExperience[]
education?: LinkedInEducation[]
languages?: string[]
}
export interface LinkedInExperience {
title: string
company: string
location?: string
startDate?: string
endDate?: string
description?: string
duration?: string
}
export interface LinkedInEducation {
school: string
degree?: string
field?: string
startYear?: number
endYear?: number
}
export interface LinkedInJobsInput extends PaginationOptions {
/** Job search keywords */
keywords: string
/** Location (e.g., "San Francisco, CA") */
location?: string
/** Maximum number of jobs to scrape */
maxResults?: number
/** Date posted filter ("past-24h", "past-week", "past-month", "any") */
datePosted?: string
/** Experience level filter */
experienceLevel?: string[]
/** Remote filter */
remote?: boolean
}
export interface LinkedInJob {
id: string
title: string
company: string
companyUrl?: string
companyLogo?: string
location: string
description: string
postedDate: string
applicants?: string
jobUrl: string
seniority?: string
employmentType?: string
jobFunctions?: string[]
industries?: string[]
salary?: string
}
export interface LinkedInPostsInput extends PaginationOptions {
/** LinkedIn profile or company URL */
profileUrl: string
/** Maximum number of posts to scrape */
maxResults?: number
}
export interface LinkedInPost extends Post {
id: string
url: string
text: string
authorName?: string
authorUrl?: string
authorHeadline?: string
likesCount: number
commentsCount: number
sharesCount?: number
timestamp: string
imageUrls?: string[]
videoUrl?: string
}
/* ============================================================================
* FUNCTIONS
* ========================================================================= */
/**
* Scrape LinkedIn profile data including email
*
* @param input - Profile scraping options
* @param options - Actor run options
* @returns LinkedIn profile data
*
* @example
* ```typescript
* // Scrape profile with email
* const profile = await scrapeLinkedInProfile({
* profileUrl: 'https://www.linkedin.com/in/exampleuser',
* includeEmail: true
* })
*
* console.log(`${profile.fullName} - ${profile.headline}`)
* console.log(`Email: ${profile.email}`)
* ```
*/
export async function scrapeLinkedInProfile(
input: LinkedInProfileInput,
options?: ActorRunOptions
): Promise<LinkedInProfile> {
const apify = new Apify()
const run = await apify.callActor('dev_fusion/Linkedin-Profile-Scraper', {
urls: [input.profileUrl],
includeEmail: input.includeEmail || false
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`LinkedIn profile scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({ limit: 1 })
if (items.length === 0) {
throw new Error(`Profile not found: ${input.profileUrl}`)
}
const profile = items[0]
return {
fullName: profile.fullName || profile.name,
headline: profile.headline,
bio: profile.about,
about: profile.about,
location: profile.location,
profileUrl: input.profileUrl,
company: profile.company,
position: profile.position || profile.headline,
email: profile.email,
phone: profile.phone,
website: profile.website,
connectionsCount: profile.connections,
followersCount: profile.followers,
skills: profile.skills,
experience: profile.experience,
education: profile.education,
languages: profile.languages
}
}
/**
* Search LinkedIn jobs
*
* @param input - Job search parameters
* @param options - Actor run options
* @returns Array of LinkedIn jobs
*
* @example
* ```typescript
* // Search for remote AI jobs
* const jobs = await searchLinkedInJobs({
* keywords: 'artificial intelligence engineer',
* location: 'United States',
* remote: true,
* maxResults: 100
* })
*
* // Filter in code - only senior roles with high applicants
* const competitiveRoles = jobs.filter(j =>
* j.seniority?.includes('Senior') &&
* parseInt(j.applicants || '0') > 100
* )
* ```
*/
export async function searchLinkedInJobs(
input: LinkedInJobsInput,
options?: ActorRunOptions
): Promise<LinkedInJob[]> {
const apify = new Apify()
const run = await apify.callActor('curious_coder/linkedin-jobs-scraper', {
keyword: input.keywords,
location: input.location,
maxItems: input.maxResults || 50,
datePosted: input.datePosted,
experienceLevel: input.experienceLevel,
remote: input.remote
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`LinkedIn jobs scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({
limit: input.maxResults || 1000,
offset: input.offset || 0
})
return items.map((job: any) => ({
id: job.jobId || job.id,
title: job.title,
company: job.company,
companyUrl: job.companyUrl,
companyLogo: job.companyLogo,
location: job.location,
description: job.description,
postedDate: job.postedDate || job.postedAt,
applicants: job.applicants,
jobUrl: job.jobUrl || job.url,
seniority: job.seniority,
employmentType: job.employmentType,
jobFunctions: job.jobFunctions,
industries: job.industries,
salary: job.salary,
url: job.jobUrl || job.url
}))
}
/**
* Scrape LinkedIn posts from a profile or company
*
* @param input - Posts scraping options
* @param options - Actor run options
* @returns Array of LinkedIn posts
*
* @example
* ```typescript
* // Scrape latest posts
* const posts = await scrapeLinkedInPosts({
* profileUrl: 'https://www.linkedin.com/in/exampleuser',
* maxResults: 50
* })
*
* // Filter in code - only high-engagement posts
* const viral = posts.filter(p =>
* p.likesCount > 100 || p.commentsCount > 20
* )
* ```
*/
export async function scrapeLinkedInPosts(
input: LinkedInPostsInput,
options?: ActorRunOptions
): Promise<LinkedInPost[]> {
const apify = new Apify()
const run = await apify.callActor('supreme_coder/linkedin-post', {
urls: [input.profileUrl],
maxPosts: input.maxResults || 50
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`LinkedIn posts scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({
limit: input.maxResults || 1000,
offset: input.offset || 0
})
return items.map((post: any) => ({
id: post.id || post.postId,
url: post.url || post.postUrl,
text: post.text || post.content,
authorName: post.authorName,
authorUrl: post.authorUrl,
authorHeadline: post.authorHeadline,
likesCount: post.likesCount || post.likes || 0,
commentsCount: post.commentsCount || post.comments || 0,
sharesCount: post.sharesCount || post.shares,
viewsCount: post.viewsCount,
timestamp: post.timestamp || post.postedAt,
imageUrls: post.images,
videoUrl: post.videoUrl,
caption: post.text
}))
}
/**
* TikTok Scraper
*
* Top Actors:
* - clockworks/tiktok-scraper (90,141 users, 4.61 rating)
* - scraptik/tiktok-api (1,329 users, 4.68 rating, $0.002/request - LOWEST COST)
*
* Extract TikTok profiles, videos, hashtags, comments without login.
*/
import { Apify } from '../../index'
import type {
UserProfile,
Post,
EngagementMetrics,
PaginationOptions,
ActorRunOptions
} from '../../types'
/* ============================================================================
* TYPES
* ========================================================================= */
export interface TikTokProfileInput {
/** TikTok username (without @) */
username: string
/** Maximum number of videos to include */
maxVideos?: number
}
export interface TikTokProfile extends UserProfile {
id: string
username: string
nickname?: string
signature?: string
verified?: boolean
followersCount: number
followingCount: number
heartCount?: number
videoCount?: number
videos?: TikTokVideo[]
}
export interface TikTokVideo extends Post {
id: string
url: string
text?: string
desc?: string
createTime: string
videoUrl?: string
coverUrl?: string
playCount?: number
likeCount: number
commentCount: number
shareCount: number
downloadCount?: number
musicTitle?: string
musicAuthor?: string
authorUsername?: string
authorNickname?: string
hashtags?: string[]
mentions?: string[]
isAd?: boolean
}
export interface TikTokHashtagInput extends PaginationOptions {
/** Hashtag (without #) */
hashtag: string
/** Maximum number of videos to scrape */
maxResults?: number
}
export interface TikTokCommentsInput extends PaginationOptions {
/** TikTok video URL */
videoUrl: string
/** Maximum number of comments to scrape */
maxResults?: number
}
export interface TikTokComment {
id: string
text: string
createTime: string
likeCount: number
replyCount?: number
username: string
userNickname?: string
}
/* ============================================================================
* FUNCTIONS
* ========================================================================= */
/**
* Scrape TikTok profile data
*
* @param input - Profile scraping options
* @param options - Actor run options
* @returns TikTok profile with videos
*
* @example
* ```typescript
* const profile = await scrapeTikTokProfile({
* username: 'exampleuser',
* maxVideos: 30
* })
*
* // Filter in code - only viral videos
* const viral = profile.videos?.filter(v => v.playCount > 1000000)
* ```
*/
export async function scrapeTikTokProfile(
input: TikTokProfileInput,
options?: ActorRunOptions
): Promise<TikTokProfile> {
const apify = new Apify()
const run = await apify.callActor('clockworks/tiktok-profile-scraper', {
profiles: [`https://www.tiktok.com/@${input.username}`],
resultsPerPage: input.maxVideos || 30
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`TikTok profile scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({ limit: 1 })
if (items.length === 0) {
throw new Error(`Profile not found: @${input.username}`)
}
const profile = items[0]
return {
id: profile.authorMeta?.id,
username: input.username,
nickname: profile.authorMeta?.name,
fullName: profile.authorMeta?.name,
bio: profile.authorMeta?.signature,
signature: profile.authorMeta?.signature,
verified: profile.authorMeta?.verified,
followersCount: profile.authorMeta?.fans || 0,
followingCount: profile.authorMeta?.following || 0,
heartCount: profile.authorMeta?.heart,
videoCount: profile.authorMeta?.video,
videos: profile.posts?.map(transformVideo)
}
}
/**
* Scrape TikTok videos by hashtag
*
* @param input - Hashtag scraping options
* @param options - Actor run options
* @returns Array of TikTok videos
*
* @example
* ```typescript
* const videos = await scrapeTikTokHashtag({
* hashtag: 'ai',
* maxResults: 100
* })
*
* // Filter in code - only high engagement
* const topVideos = videos
* .filter(v => v.likeCount > 10000)
* .sort((a, b) => b.likeCount - a.likeCount)
* .slice(0, 10)
* ```
*/
export async function scrapeTikTokHashtag(
input: TikTokHashtagInput,
options?: ActorRunOptions
): Promise<TikTokVideo[]> {
const apify = new Apify()
const run = await apify.callActor('clockworks/tiktok-hashtag-scraper', {
hashtags: [input.hashtag],
resultsPerPage: input.maxResults || 100
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`TikTok hashtag scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({
limit: input.maxResults || 1000,
offset: input.offset || 0
})
return items.map(transformVideo)
}
/**
* Scrape TikTok comments from a video
*
* @param input - Comment scraping options
* @param options - Actor run options
* @returns Array of comments
*
* @example
* ```typescript
* const comments = await scrapeTikTokComments({
* videoUrl: 'https://www.tiktok.com/@user/video/123',
* maxResults: 200
* })
*
* // Filter in code - only popular comments
* const popular = comments.filter(c => c.likeCount > 50)
* ```
*/
export async function scrapeTikTokComments(
input: TikTokCommentsInput,
options?: ActorRunOptions
): Promise<TikTokComment[]> {
const apify = new Apify()
const run = await apify.callActor('clockworks/tiktok-comments-scraper', {
postURLs: [input.videoUrl],
maxComments: input.maxResults || 100
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`TikTok comments scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({
limit: input.maxResults || 1000,
offset: input.offset || 0
})
return items.map((comment: any) => ({
id: comment.cid,
text: comment.text,
createTime: comment.createTime,
likeCount: comment.diggCount || 0,
replyCount: comment.replyCommentTotal,
username: comment.user?.uniqueId,
userNickname: comment.user?.nickname
}))
}
/* ============================================================================
* HELPERS
* ========================================================================= */
function transformVideo(video: any): TikTokVideo {
return {
id: video.id,
url: video.webVideoUrl || `https://www.tiktok.com/@${video.authorMeta?.name}/video/${video.id}`,
text: video.text,
desc: video.text,
caption: video.text,
createTime: video.createTime,
timestamp: video.createTime,
videoUrl: video.videoUrl,
coverUrl: video.covers?.default,
playCount: video.playCount,
viewsCount: video.playCount,
likeCount: video.diggCount || 0,
likesCount: video.diggCount || 0,
commentCount: video.commentCount || 0,
commentsCount: video.commentCount || 0,
shareCount: video.shareCount || 0,
sharesCount: video.shareCount || 0,
downloadCount: video.downloadCount,
musicTitle: video.musicMeta?.musicName,
musicAuthor: video.musicMeta?.musicAuthor,
authorUsername: video.authorMeta?.name,
authorNickname: video.authorMeta?.nickName,
hashtags: video.hashtags?.map((h: any) => h.name),
mentions: video.mentions,
isAd: video.isAd
}
}
/**
* Twitter/X Scraper
*
* Top Actor:
* - apidojo/twitter-scraper-lite (Unlimited, no rate limits, event-based pricing)
*
* Extract Twitter/X profiles, tweets, followers, and search results.
*/
import { Apify } from '../../index'
import type {
UserProfile,
Post,
PaginationOptions,
ActorRunOptions
} from '../../types'
/* ============================================================================
* TYPES
* ========================================================================= */
export interface TwitterProfileInput {
/** Twitter username (without @) */
username: string
/** Include tweets in profile response */
includeTweets?: boolean
/** Maximum number of tweets to fetch */
maxTweets?: number
}
export interface TwitterProfile extends UserProfile {
username: string
displayName: string
bio?: string
location?: string
website?: string
profileImageUrl?: string
bannerImageUrl?: string
followersCount?: number
followingCount?: number
tweetsCount?: number
verified?: boolean
createdAt?: string
latestTweets?: TwitterTweet[]
}
export interface TwitterTweetsInput extends PaginationOptions {
/** Twitter username (without @) */
username: string
/** Maximum number of tweets to scrape */
maxTweets?: number
/** Include replies */
includeReplies?: boolean
/** Include retweets */
includeRetweets?: boolean
}
export interface TwitterSearchInput extends PaginationOptions {
/** Search query */
query: string
/** Maximum number of tweets to return */
maxTweets?: number
/** Search type: "Latest", "Top", "People", "Photos", "Videos" */
searchType?: string
}
export interface TwitterTweet extends Post {
id: string
url: string
text: string
authorUsername: string
authorDisplayName: string
timestamp: string
likesCount: number
retweetsCount: number
repliesCount: number
viewsCount?: number
hashtags?: string[]
mentions?: string[]
imageUrls?: string[]
videoUrl?: string
isRetweet?: boolean
isReply?: boolean
quotedTweet?: TwitterTweet
}
/* ============================================================================
* FUNCTIONS
* ========================================================================= */
/**
* Scrape Twitter/X profile data
*
* @param input - Profile scraping options
* @param options - Actor run options
* @returns Twitter profile data
*
* @example
* ```typescript
* // Scrape profile with latest tweets
* const profile = await scrapeTwitterProfile({
* username: 'exampleuser',
* includeTweets: true,
* maxTweets: 20
* })
*
* console.log(`${profile.displayName} (@${profile.username})`)
* console.log(`Followers: ${profile.followersCount}`)
* console.log(`Latest tweets: ${profile.latestTweets?.length}`)
* ```
*/
export async function scrapeTwitterProfile(
input: TwitterProfileInput,
options?: ActorRunOptions
): Promise<TwitterProfile> {
const apify = new Apify()
const run = await apify.callActor('apidojo/twitter-scraper-lite', {
mode: 'profile',
username: input.username,
maxTweets: input.includeTweets ? (input.maxTweets || 20) : 0
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`Twitter profile scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({ limit: 100 })
if (items.length === 0) {
throw new Error(`Profile not found: @${input.username}`)
}
// First item is profile, rest are tweets
const profileData = items[0]
const tweets = items.slice(1)
return {
username: profileData.username || input.username,
displayName: profileData.name || profileData.displayName,
bio: profileData.description || profileData.bio,
location: profileData.location,
website: profileData.url || profileData.website,
profileImageUrl: profileData.profileImageUrl,
bannerImageUrl: profileData.bannerImageUrl,
followersCount: profileData.followersCount || profileData.followers,
followingCount: profileData.followingCount || profileData.following,
tweetsCount: profileData.tweetsCount || profileData.tweets,
verified: profileData.verified || profileData.isVerified,
createdAt: profileData.createdAt,
latestTweets: tweets.map(mapToTwitterTweet)
}
}
/**
* Scrape tweets from a Twitter/X user
*
* @param input - Tweets scraping options
* @param options - Actor run options
* @returns Array of tweets
*
* @example
* ```typescript
* // Get latest tweets
* const tweets = await scrapeTwitterTweets({
* username: 'exampleuser',
* maxTweets: 100,
* includeReplies: false
* })
*
* // Filter in code - only high engagement
* const viral = tweets.filter(t =>
* t.likesCount > 100 || t.retweetsCount > 50
* )
* ```
*/
export async function scrapeTwitterTweets(
input: TwitterTweetsInput,
options?: ActorRunOptions
): Promise<TwitterTweet[]> {
const apify = new Apify()
const run = await apify.callActor('apidojo/twitter-scraper-lite', {
mode: 'tweets',
username: input.username,
maxTweets: input.maxTweets || 100,
includeReplies: input.includeReplies !== false,
includeRetweets: input.includeRetweets !== false
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`Twitter tweets scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({
limit: input.maxTweets || 1000,
offset: input.offset || 0
})
return items.map(mapToTwitterTweet)
}
/**
* Search Twitter/X for tweets
*
* @param input - Search parameters
* @param options - Actor run options
* @returns Array of tweets matching search
*
* @example
* ```typescript
* // Search for AI security tweets
* const tweets = await searchTwitter({
* query: 'AI security',
* maxTweets: 50,
* searchType: 'Latest'
* })
*
* // Filter in code - only from verified users
* const verifiedTweets = tweets.filter(t =>
* t.authorVerified === true
* )
* ```
*/
export async function searchTwitter(
input: TwitterSearchInput,
options?: ActorRunOptions
): Promise<TwitterTweet[]> {
const apify = new Apify()
const run = await apify.callActor('apidojo/twitter-scraper-lite', {
mode: 'search',
query: input.query,
maxTweets: input.maxTweets || 100,
searchType: input.searchType || 'Latest'
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`Twitter search failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({
limit: input.maxTweets || 1000,
offset: input.offset || 0
})
return items.map(mapToTwitterTweet)
}
/* ============================================================================
* HELPERS
* ========================================================================= */
function mapToTwitterTweet(tweet: any): TwitterTweet {
return {
id: tweet.id || tweet.tweetId,
url: tweet.url || `https://twitter.com/${tweet.authorUsername}/status/${tweet.id}`,
text: tweet.text || tweet.fullText,
authorUsername: tweet.authorUsername || tweet.username,
authorDisplayName: tweet.authorName || tweet.displayName,
timestamp: tweet.createdAt || tweet.timestamp,
likesCount: tweet.likesCount || tweet.likes || 0,
retweetsCount: tweet.retweetsCount || tweet.retweets || 0,
repliesCount: tweet.repliesCount || tweet.replies || 0,
viewsCount: tweet.viewsCount || tweet.views,
commentsCount: tweet.repliesCount || tweet.replies || 0,
hashtags: tweet.hashtags,
mentions: tweet.mentions,
imageUrls: tweet.media?.filter((m: any) => m.type === 'photo').map((m: any) => m.url),
videoUrl: tweet.media?.find((m: any) => m.type === 'video')?.url,
isRetweet: tweet.isRetweet,
isReply: tweet.isReplyTo !== undefined,
quotedTweet: tweet.quotedTweet ? mapToTwitterTweet(tweet.quotedTweet) : undefined,
caption: tweet.text || tweet.fullText
}
}
/**
* YouTube Scraper
*
* Top Actors:
* - streamers/youtube-scraper (40,455 users, 4.40 rating, $0.005/video)
* - apidojo/youtube-scraper (4,336 users, 3.88 rating, $0.50/1k videos)
*
* Extract YouTube channels, videos, comments - no API quotas/limits!
*/
import { Apify } from '../../index'
import type {
UserProfile,
Post,
EngagementMetrics,
PaginationOptions,
ActorRunOptions
} from '../../types'
/* ============================================================================
* TYPES
* ========================================================================= */
export interface YouTubeChannelInput {
/** YouTube channel URL or ID */
channelUrl: string
/** Maximum number of videos to include */
maxVideos?: number
}
export interface YouTubeChannel extends UserProfile {
id: string
title: string
url: string
description?: string
subscribersCount?: number
videosCount?: number
viewsCount?: number
joinedDate?: string
country?: string
thumbnailUrl?: string
bannerUrl?: string
verified?: boolean
videos?: YouTubeVideo[]
}
export interface YouTubeVideo extends Post {
id: string
url: string
title: string
description?: string
channelId?: string
channelTitle?: string
channelUrl?: string
publishedAt: string
viewsCount: number
likesCount?: number
commentsCount?: number
duration?: string
thumbnailUrl?: string
tags?: string[]
category?: string
}
export interface YouTubeSearchInput extends PaginationOptions {
/** Search query */
query: string
/** Maximum number of videos */
maxResults?: number
/** Upload date filter */
uploadDate?: 'hour' | 'today' | 'week' | 'month' | 'year'
/** Duration filter */
duration?: 'short' | 'medium' | 'long'
/** Sort by */
sortBy?: 'relevance' | 'date' | 'viewCount' | 'rating'
}
export interface YouTubeCommentsInput extends PaginationOptions {
/** YouTube video URL or ID */
videoUrl: string
/** Maximum number of comments */
maxResults?: number
}
export interface YouTubeComment {
id: string
text: string
authorName: string
authorChannelUrl?: string
likesCount: number
replyCount?: number
publishedAt: string
}
/* ============================================================================
* FUNCTIONS
* ========================================================================= */
/**
* Scrape YouTube channel data
*
* @param input - Channel scraping options
* @param options - Actor run options
* @returns YouTube channel with videos
*
* @example
* ```typescript
* const channel = await scrapeYouTubeChannel({
* channelUrl: 'https://www.youtube.com/@exampleuser',
* maxVideos: 50
* })
*
* // Filter in code - only high-performing videos
* const topVideos = channel.videos
* ?.filter(v => v.viewsCount > 10000)
* .sort((a, b) => b.viewsCount - a.viewsCount)
* .slice(0, 10)
* ```
*/
export async function scrapeYouTubeChannel(
input: YouTubeChannelInput,
options?: ActorRunOptions
): Promise<YouTubeChannel> {
const apify = new Apify()
const run = await apify.callActor('streamers/youtube-channel-scraper', {
startUrls: [input.channelUrl],
maxResults: input.maxVideos || 50
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`YouTube channel scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems()
if (items.length === 0) {
throw new Error(`Channel not found: ${input.channelUrl}`)
}
// First item is channel info, rest are videos
const channelData = items[0]
const videos = items.slice(1).map(transformVideo)
return {
id: channelData.channelId,
title: channelData.title,
fullName: channelData.title,
url: channelData.url || input.channelUrl,
description: channelData.description,
bio: channelData.description,
subscribersCount: channelData.numberOfSubscribers,
followersCount: channelData.numberOfSubscribers,
videosCount: channelData.numberOfVideos,
viewsCount: channelData.numberOfViews,
joinedDate: channelData.joinedDate,
country: channelData.country,
thumbnailUrl: channelData.thumbnailUrl,
bannerUrl: channelData.bannerUrl,
verified: channelData.verified,
videos
}
}
/**
* Search YouTube videos
*
* @param input - Search parameters
* @param options - Actor run options
* @returns Array of YouTube videos
*
* @example
* ```typescript
* const videos = await searchYouTube({
* query: 'artificial intelligence tutorial',
* maxResults: 100,
* uploadDate: 'month',
* sortBy: 'viewCount'
* })
*
* // Filter in code - only videos with high engagement
* const engaging = videos.filter(v =>
* v.viewsCount > 50000 &&
* (v.likesCount || 0) > 1000
* )
* ```
*/
export async function searchYouTube(
input: YouTubeSearchInput,
options?: ActorRunOptions
): Promise<YouTubeVideo[]> {
const apify = new Apify()
const run = await apify.callActor('streamers/youtube-scraper', {
searchKeywords: input.query,
maxResults: input.maxResults || 50,
uploadDate: input.uploadDate,
videoDuration: input.duration,
sortBy: input.sortBy || 'relevance'
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`YouTube search failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({
limit: input.maxResults || 1000,
offset: input.offset || 0
})
return items.map(transformVideo)
}
/**
* Scrape YouTube comments from a video
*
* @param input - Comment scraping options
* @param options - Actor run options
* @returns Array of comments
*
* @example
* ```typescript
* const comments = await scrapeYouTubeComments({
* videoUrl: 'https://www.youtube.com/watch?v=ABC123',
* maxResults: 500
* })
*
* // Filter in code - only highly-liked comments
* const popular = comments
* .filter(c => c.likesCount > 100)
* .sort((a, b) => b.likesCount - a.likesCount)
* ```
*/
export async function scrapeYouTubeComments(
input: YouTubeCommentsInput,
options?: ActorRunOptions
): Promise<YouTubeComment[]> {
const apify = new Apify()
const run = await apify.callActor('streamers/youtube-comments-scraper', {
startUrls: [input.videoUrl],
maxComments: input.maxResults || 100
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`YouTube comments scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({
limit: input.maxResults || 1000,
offset: input.offset || 0
})
return items.map((comment: any) => ({
id: comment.id,
text: comment.text,
authorName: comment.authorText,
authorChannelUrl: comment.authorChannelUrl,
likesCount: comment.likesCount || 0,
replyCount: comment.replyCount,
publishedAt: comment.publishedTimeText
}))
}
/* ============================================================================
* HELPERS
* ========================================================================= */
function transformVideo(video: any): YouTubeVideo {
return {
id: video.id,
url: video.url || `https://www.youtube.com/watch?v=${video.id}`,
title: video.title,
text: video.title,
description: video.description,
channelId: video.channelId,
channelTitle: video.channelName || video.channelTitle,
channelUrl: video.channelUrl,
publishedAt: video.date || video.publishedAt,
timestamp: video.date || video.publishedAt,
viewsCount: video.views || video.viewsCount || 0,
likesCount: video.likes || video.likesCount,
commentsCount: video.numberOfComments || video.commentsCount,
duration: video.duration,
thumbnailUrl: video.thumbnail || video.thumbnailUrl,
tags: video.tags,
category: video.category
}
}
/**
* Web Scraping Actors
*
* - Web Scraper (94k users - general purpose)
*/
export * from './web-scraper'
/**
* Web Scraper (General Purpose)
*
* Apify Actor: apify/web-scraper (94,522 users, 4.39 rating)
* Pricing: FREE - only pay for Apify platform usage
*
* Crawl any website and extract structured data using JavaScript functions.
* Most versatile actor - handles ANY website!
*/
import { Apify } from '../../index'
import type {
PaginationOptions,
ActorRunOptions
} from '../../types'
/* ============================================================================
* TYPES
* ========================================================================= */
export interface WebScraperInput {
/** URLs to start crawling from */
startUrls: string[]
/** JavaScript function to extract data from each page */
pageFunction?: string
/** CSS selector for links to follow */
linkSelector?: string
/** Pseudo-URLs to match for crawling */
pseudoUrls?: string[]
/** Maximum pages to crawl */
maxPagesPerCrawl?: number
/** Maximum crawling depth */
maxCrawlingDepth?: number
/** Proxy configuration */
useProxy?: boolean
/** Wait for dynamic content (ms) */
waitUntil?: 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2'
}
export interface ScrapedPage {
url: string
title?: string
html?: string
text?: string
[key: string]: any // Custom extracted data
}
/* ============================================================================
* FUNCTIONS
* ========================================================================= */
/**
* Scrape data from websites using custom extraction logic
*
* @param input - Web scraping configuration
* @param options - Actor run options
* @returns Array of scraped pages with extracted data
*
* @example
* ```typescript
* // Scrape product listings
* const products = await scrapeWebsite({
* startUrls: ['https://example.com/products'],
* linkSelector: 'a.product-link',
* maxPagesPerCrawl: 100,
* pageFunction: `
* async function pageFunction(context) {
* const { request, $, log } = context
*
* // Extract data using jQuery-like selectors
* return {
* url: request.url,
* title: $('h1.product-title').text(),
* price: $('span.price').text(),
* description: $('.description').text(),
* inStock: $('.in-stock').length > 0
* }
* }
* `
* })
*
* // Filter in code - only available products under $100
* const affordable = products.filter(p =>
* p.inStock &&
* parseFloat(p.price.replace('$', '')) < 100
* )
* ```
*
* @example
* ```typescript
* // Simple HTML/text extraction
* const pages = await scrapeWebsite({
* startUrls: ['https://blog.example.com'],
* linkSelector: 'a.post-link',
* maxPagesPerCrawl: 50,
* pageFunction: `
* async function pageFunction(context) {
* const { request, $, log } = context
*
* return {
* url: request.url,
* title: $('h1').first().text(),
* author: $('.author').text(),
* date: $('.date').text(),
* content: $('.post-content').text(),
* tags: $('.tag').map((i, el) => $(el).text()).get()
* }
* }
* `
* })
*
* // Filter in code - only recent posts
* const recent = pages.filter(p => {
* const postDate = new Date(p.date)
* const monthAgo = Date.now() - (30 * 24 * 60 * 60 * 1000)
* return postDate.getTime() > monthAgo
* })
* ```
*/
export async function scrapeWebsite(
input: WebScraperInput,
options?: ActorRunOptions
): Promise<ScrapedPage[]> {
const apify = new Apify()
// Default page function that extracts basic data
const defaultPageFunction = `
async function pageFunction(context) {
const { request, $, log } = context
return {
url: request.url,
title: $('title').text() || $('h1').first().text(),
text: $('body').text().trim()
}
}
`
const run = await apify.callActor('apify/web-scraper', {
startUrls: input.startUrls.map(url => ({ url })),
pageFunction: input.pageFunction || defaultPageFunction,
linkSelector: input.linkSelector,
pseudoUrls: input.pseudoUrls?.map(pattern => ({ purl: pattern })),
maxPagesPerCrawl: input.maxPagesPerCrawl || 100,
maxCrawlingDepth: input.maxCrawlingDepth || 0,
useProxy: input.useProxy || false,
waitUntil: input.waitUntil || 'networkidle2'
}, options)
await apify.waitForRun(run.id)
const finalRun = await apify.getRun(run.id)
if (finalRun.status !== 'SUCCEEDED') {
throw new Error(`Web scraping failed: ${finalRun.status}`)
}
const dataset = apify.getDataset(finalRun.defaultDatasetId)
const items = await dataset.listItems({
limit: input.maxPagesPerCrawl || 10000
})
return items as ScrapedPage[]
}
/**
* Extract structured data from a single page
*
* @param url - URL to scrape
* @param pageFunction - JavaScript function to extract data
* @param options - Actor run options
* @returns Extracted data
*
* @example
* ```typescript
* // Scrape a single product page
* const product = await scrapePage(
* 'https://example.com/product/123',
* `async function pageFunction(context) {
* const { $, request } = context
* return {
* name: $('h1.product-name').text(),
* price: $('span.price').text(),
* rating: parseFloat($('.rating').attr('data-rating')),
* reviews: parseInt($('.review-count').text()),
* images: $('img.product-image')
* .map((i, el) => $(el).attr('src'))
* .get()
* }
* }`
* )
*
* console.log(`${product.name} - ${product.price}`)
* console.log(`Rating: ${product.rating}/5 (${product.reviews} reviews)`)
* ```
*/
export async function scrapePage(
url: string,
pageFunction: string,
options?: ActorRunOptions
): Promise<ScrapedPage> {
const results = await scrapeWebsite({
startUrls: [url],
maxPagesPerCrawl: 1,
pageFunction
}, options)
if (results.length === 0) {
throw new Error(`Failed to scrape page: ${url}`)
}
return results[0]
}
#!/usr/bin/env bun
/**
* Comparison Test: MCP vs Code-First Apify
*
* Demonstrates the difference in approach and token usage between
* traditional MCP tool calls and code-first execution.
*/
import { Apify } from '../index'
// Utility to estimate token count
function estimateTokens(data: any): number {
const str = JSON.stringify(data)
// Rough estimate: ~4 characters per token
return Math.ceil(str.length / 4)
}
async function demonstrateMCPApproach() {
console.log('=== MCP APPROACH ===\n')
console.log('Traditional MCP flow with multiple round-trips through model context:\n')
console.log('Step 1: mcp__Apify__search-actors')
console.log(' Input: { search: "instagram scraper", limit: 10 }')
console.log(' → Tool definitions loaded: ~5,000 tokens')
console.log(' → Search results returned: ~1,000 tokens')
console.log(' → Results pass through model context')
console.log('\nStep 2: mcp__Apify__call-actor')
console.log(' Input: { actor: "apify/instagram-scraper", input: {...} }')
console.log(' → Run information returned: ~1,000 tokens')
console.log(' → Results pass through model context')
console.log('\nStep 3: mcp__Apify__get-actor-output')
console.log(' Input: { datasetId: "xyz123" }')
console.log(' → FULL dataset returned: ~50,000 tokens (100 items)')
console.log(' → ALL results pass through model context')
console.log(' → Model must filter in subsequent reasoning step')
console.log('\nStep 4: Model reasoning to filter')
console.log(' → Additional model call to process and filter')
console.log(' → Context includes all 100 items again')
console.log('\n📊 MCP Total Token Usage:')
console.log(' Tool definitions: 5,000 tokens')
console.log(' Search results: 1,000 tokens')
console.log(' Run info: 1,000 tokens')
console.log(' Full dataset: 50,000 tokens')
console.log(' ────────────────────────────────')
console.log(' TOTAL: ~57,000 tokens')
console.log(' Plus additional reasoning overhead!\n')
}
async function demonstrateCodeFirstApproach() {
console.log('=== CODE-FIRST APPROACH ===\n')
console.log('Direct code execution with in-code filtering:\n')
const apify = new Apify()
console.log('Step 1: Model reads README.md for API discovery')
console.log(' → README.md content: ~200 tokens')
console.log(' → Progressive disclosure (only load what\'s needed)')
console.log('\nStep 2: Model writes code to execute operations')
const codeExample = `
import { Apify } from '~/.claude/filesystem-mcps/apify'
const apify = new Apify()
// All operations in code - no intermediate context bloat
const actors = await apify.search("instagram scraper")
const run = await apify.callActor(actors[0].id, {
profiles: ["target"],
resultsLimit: 100
})
// Wait for completion
await apify.waitForRun(actors[0].id, run.id)
// Get dataset
const dataset = apify.getDataset(run.defaultDatasetId)
const items = await dataset.listItems()
// CRITICAL: Filter in code BEFORE returning to model
const yesterday = Date.now() - 86400000
const filtered = items
.filter(post => post.likesCount > 1000)
.filter(post => post.timestamp > yesterday)
.slice(0, 10)
// Only 10 filtered results reach model context
return filtered
`.trim()
console.log(' Code to execute (~300 tokens):')
console.log(' ' + codeExample.split('\n').join('\n '))
console.log('\nStep 3: Code executes in bash environment')
console.log(' → All operations happen locally')
console.log(' → Intermediate results NEVER enter model context')
console.log(' → Filtering happens in execution environment')
console.log('\nStep 4: Only filtered results return to model')
console.log(' → Filtered dataset: 10 items (~500 tokens)')
console.log(' → Model sees only what it needs')
console.log('\n📊 Code-First Total Token Usage:')
console.log(' README discovery: 200 tokens')
console.log(' Code execution: 300 tokens')
console.log(' Filtered results: 500 tokens')
console.log(' ────────────────────────────────')
console.log(' TOTAL: ~1,000 tokens')
console.log('\n 💰 TOKEN SAVINGS: 98.2% reduction!')
console.log(' ⚡ PERFORMANCE: Faster (no model round-trips)')
console.log(' 🔒 PRIVACY: Intermediate data never in model context\n')
}
async function demonstrateFilteringComparison() {
console.log('=== FILTERING COMPARISON ===\n')
// Simulate a dataset of 100 items
const fullDataset = Array.from({ length: 100 }, (_, i) => ({
id: `post_${i}`,
username: `user${i}`,
text: `This is post ${i} with some content`,
likesCount: Math.floor(Math.random() * 5000),
timestamp: Date.now() - Math.random() * 86400000 * 7,
url: `https://instagram.com/p/${i}`
}))
// Filter to top 10 high-engagement recent posts
const yesterday = Date.now() - 86400000
const filtered = fullDataset
.filter(post => post.likesCount > 1000)
.filter(post => post.timestamp > yesterday)
.sort((a, b) => b.likesCount - a.likesCount)
.slice(0, 10)
const fullTokens = estimateTokens(fullDataset)
const filteredTokens = estimateTokens(filtered)
const savings = ((fullTokens - filteredTokens) / fullTokens * 100).toFixed(1)
console.log('Dataset Size Comparison:')
console.log(` Full dataset: ${fullDataset.length} items (${fullTokens} tokens)`)
console.log(` Filtered dataset: ${filtered.length} items (${filteredTokens} tokens)`)
console.log(` Reduction: ${savings}% fewer tokens\n`)
console.log('MCP Approach:')
console.log(' 1. Return all 100 items to model (${fullTokens} tokens)')
console.log(' 2. Model reasons about filtering criteria')
console.log(' 3. Model makes another call to filter')
console.log(' 4. All 100 items in context again during filtering')
console.log(` Total: ~${fullTokens * 2} tokens (dataset appears 2x in context)\n`)
console.log('Code-First Approach:')
console.log(' 1. Filter executed in code environment')
console.log(' 2. Only 10 items returned to model')
console.log(` Total: ~${filteredTokens} tokens\n`)
console.log(`💡 Key Insight: Code-first prevents ${fullDataset.length - filtered.length} irrelevant items`)
console.log(' from ever entering the model context!\n')
}
async function main() {
console.log('\n╔═══════════════════════════════════════════════════════════╗')
console.log('║ MCP vs Code-First Comparison: Apify Integration ║')
console.log('╚═══════════════════════════════════════════════════════════╝\n')
await demonstrateMCPApproach()
console.log('\n' + '─'.repeat(60) + '\n')
await demonstrateCodeFirstApproach()
console.log('\n' + '─'.repeat(60) + '\n')
await demonstrateFilteringComparison()
console.log('\n' + '─'.repeat(60) + '\n')
console.log('=== CONCLUSION ===\n')
console.log('Code-first Apify integration provides:')
console.log(' ✅ 98%+ token reduction through in-code filtering')
console.log(' ✅ Faster execution (no model round-trips for control flow)')
console.log(' ✅ Better privacy (intermediate data stays in execution env)')
console.log(' ✅ Progressive disclosure (load only what you need)')
console.log(' ✅ More maintainable (standard TypeScript, not tool schemas)\n')
console.log('When to use:')
console.log(' • Data-heavy operations (scraping, large datasets)')
console.log(' • Operations requiring filtering/transformation')
console.log(' • Multiple sequential operations')
console.log(' • Privacy-sensitive workflows\n')
}
// Run if executed directly
if (import.meta.main) {
main()
}
export { main }
#!/usr/bin/env bun
/**
* Example: Instagram Scraper with Code-First Apify
*
* Demonstrates token savings through in-code filtering:
* - MCP approach: ~57,000 tokens
* - Code-first: ~1,000 tokens (98.2% reduction)
*/
import { Apify } from '../index'
async function main() {
console.log('=== Apify Code-First Example: Instagram Scraper ===\n')
// Initialize client (uses APIFY_TOKEN from environment)
const apify = new Apify()
try {
// Step 1: Search for Instagram scraper actors
console.log('1. Searching for Instagram scraper actors...')
const actors = await apify.search('instagram scraper', { limit: 5 })
console.log(` Found ${actors.length} actors:`)
actors.forEach((actor, i) => {
console.log(` ${i + 1}. ${actor.username}/${actor.name}`)
console.log(` ${actor.title}`)
console.log(` Stats: ${actor.stats.runs.total} runs, ${actor.stats.users.total} users\n`)
})
// Select the most popular actor
const selectedActor = actors[0]
console.log(` Selected: ${selectedActor.username}/${selectedActor.name}\n`)
// Step 2: Call the actor (execute scraping)
console.log('2. Calling actor to scrape Instagram profiles...')
console.log(' (This is a dry run - modify input for real scraping)')
// Example input - modify for actual use
const input = {
// Instagram profile usernames to scrape
profiles: ['example'],
// Limit results to avoid excessive runtime/costs
resultsLimit: 50,
// Other common options
// searchLimit: 10,
// proxy: { useApifyProxy: true }
}
console.log(' Input:', JSON.stringify(input, null, 2))
console.log(' Note: Using dry run mode (not actually executing)\n')
// Uncomment to actually run:
// const run = await apify.callActor(selectedActor.id, input, {
// memory: 2048,
// timeout: 300
// })
//
// console.log(` Run started: ${run.id}`)
// console.log(` Status: ${run.status}`)
// console.log(` Container URL: ${run.containerUrl}\n`)
//
// // Step 3: Wait for completion
// console.log('3. Waiting for actor run to complete...')
// await apify.waitForRun(selectedActor.id, run.id, { waitSecs: 300 })
//
// const finalRun = await apify.getRun(selectedActor.id, run.id)
// console.log(` Final status: ${finalRun.status}`)
//
// if (finalRun.status !== 'SUCCEEDED') {
// console.error(' Actor run failed!')
// process.exit(1)
// }
//
// // Step 4: Get dataset and filter results IN CODE
// console.log('\n4. Fetching and filtering results...')
// const dataset = apify.getDataset(finalRun.defaultDatasetId)
//
// // Get all items
// const allItems = await dataset.listItems({ limit: 100 })
// console.log(` Total items retrieved: ${allItems.length}`)
//
// // KEY: Filter in code BEFORE returning to model context
// const yesterday = Date.now() - 86400000 // 24 hours ago
// const filtered = allItems
// .filter(post => post.likesCount > 1000) // High engagement
// .filter(post => post.timestamp > yesterday) // Recent
// .sort((a, b) => b.likesCount - a.likesCount) // Top first
// .slice(0, 10) // Top 10
//
// console.log(` Filtered to top ${filtered.length} high-engagement recent posts\n`)
//
// // Step 5: Show token savings
// const estimateTokens = (data: any) => {
// return Math.ceil(JSON.stringify(data).length / 4)
// }
//
// const mcpTokens = estimateTokens(allItems)
// const codeTokens = estimateTokens(filtered)
// const savings = ((mcpTokens - codeTokens) / mcpTokens * 100).toFixed(1)
//
// console.log('=== Token Savings ===')
// console.log(`MCP approach (all items): ~${mcpTokens} tokens`)
// console.log(`Code-first (filtered): ~${codeTokens} tokens`)
// console.log(`Savings: ${savings}%`)
//
// // Return filtered results (only these reach model context)
// return filtered
console.log('3. Dry run complete!')
console.log(' Uncomment the code above to actually execute the scraper.')
console.log(' Make sure to:')
console.log(' - Set valid Instagram profile usernames')
console.log(' - Have sufficient Apify credits')
console.log(' - Review actor documentation for input schema\n')
} catch (error) {
console.error('Error:', error instanceof Error ? error.message : error)
process.exit(1)
}
}
// Run if executed directly
if (import.meta.main) {
main()
}
export { main }
#!/usr/bin/env bun
/**
* Smoke Test: Verify Apify Code-First API Works
*
* Tests basic functionality without executing expensive operations.
*/
import { Apify } from '../index'
async function main() {
console.log('=== Apify Code-First Smoke Test ===\n')
if (!process.env.APIFY_TOKEN && !process.env.APIFY_API_KEY) {
console.error('❌ APIFY_TOKEN or APIFY_API_KEY not set in environment')
console.error(' Add to ${PAI_DIR}/.env: APIFY_TOKEN=apify_api_xxxxx')
console.error(' Or: APIFY_API_KEY=apify_api_xxxxx\n')
process.exit(1)
}
const apify = new Apify()
try {
// Test 1: Search for actors
console.log('Test 1: Searching for actors...')
const actors = await apify.search('web scraper', { limit: 3 })
if (actors.length === 0) {
console.error('❌ No actors found - API may not be working')
process.exit(1)
}
console.log(`✅ Found ${actors.length} actors:`)
actors.forEach((actor, i) => {
console.log(` ${i + 1}. ${actor.username}/${actor.name}`)
console.log(` ${actor.title}`)
if (actor.stats?.totalRuns) {
console.log(` Runs: ${actor.stats.totalRuns}`)
}
})
console.log()
// Test 2: Verify types
console.log('Test 2: Verifying TypeScript types...')
const firstActor = actors[0]
if (!firstActor.id || !firstActor.name || !firstActor.username) {
console.error('❌ Actor object missing required fields')
process.exit(1)
}
console.log('✅ Actor types correct')
console.log()
// Test 3: Test token estimation
console.log('Test 3: Token estimation...')
const estimateTokens = (data: any) => {
return Math.ceil(JSON.stringify(data).length / 4)
}
const tokens = estimateTokens(actors)
console.log(`✅ ${actors.length} actors = ~${tokens} tokens`)
console.log()
console.log('=== ALL TESTS PASSED ===\n')
console.log('✅ Apify code-first API is working correctly')
console.log('✅ Ready to use for scraping operations')
console.log('✅ Token savings will apply when filtering datasets\n')
} catch (error) {
console.error('❌ Test failed:', error instanceof Error ? error.message : error)
process.exit(1)
}
}
if (import.meta.main) {
main()
}
export { main }
/**
* Apify Code-First Interface
*
* Replaces token-heavy MCP calls with direct code execution.
* Enables in-code filtering and control flow for massive token savings.
*/
import { ApifyClient } from 'apify-client'
export interface Actor {
id: string
name: string
username: string
title: string
description?: string
createdAt?: string
modifiedAt?: string
stats?: {
totalRuns?: number
lastRunStartedAt?: string
}
}
export interface ActorRun {
id: string
actorId: string
status: 'READY' | 'RUNNING' | 'SUCCEEDED' | 'FAILED' | 'TIMED-OUT' | 'ABORTED'
startedAt: string
finishedAt?: string
defaultDatasetId: string
defaultKeyValueStoreId: string
buildNumber?: string
exitCode?: number
containerUrl?: string
output?: any
}
export interface DatasetOptions {
offset?: number
limit?: number
fields?: string[]
omit?: string[]
clean?: boolean
}
/**
* Main Apify client for code-first operations
*/
export class Apify {
private client: ApifyClient
constructor(token?: string) {
this.client = new ApifyClient({
token: token || process.env.APIFY_TOKEN || process.env.APIFY_API_KEY
})
}
/**
* Search for actors by keyword
*
* Fetches actors and filters client-side by query (name, title, description).
* For better performance with many actors, consider listing all and caching.
*
* @param query - Search query (actor name, description, etc.)
* @param options - Search options
* @returns Array of matching actors
*/
async search(query: string, options?: {
limit?: number
offset?: number
}): Promise<Actor[]> {
// Fetch more actors than needed to ensure we get enough matches
const fetchLimit = Math.max((options?.limit ?? 10) * 3, 30)
const { items } = await this.client.actors().list({
limit: fetchLimit,
offset: options?.offset ?? 0
})
// Filter client-side by query
// Match if ANY word in query appears in actor fields
const queryWords = query.toLowerCase().split(/\s+/)
const filtered = items.filter((actor: any) => {
const name = (actor.name || '').toLowerCase()
const title = (actor.title || '').toLowerCase()
const description = (actor.description || '').toLowerCase()
const username = (actor.username || '').toLowerCase()
const searchText = `${name} ${title} ${description} ${username}`
// Match if any query word is found
return queryWords.some(word => searchText.includes(word))
})
// Return requested number of matches
return filtered.slice(0, options?.limit ?? 10) as Actor[]
}
/**
* Call (execute) an actor
*
* @param actorId - Actor ID or "username/actor-name"
* @param input - Actor input configuration
* @param options - Runtime options (memory, timeout)
* @returns Actor run information
*/
async callActor(
actorId: string,
input: any,
options?: {
memory?: number // Memory in MB (128, 256, 512, 1024, etc.)
timeout?: number // Timeout in seconds
build?: string // Build number or tag
}
): Promise<ActorRun> {
const run = await this.client.actor(actorId).call(input, {
memory: options?.memory,
timeout: options?.timeout,
build: options?.build
})
return run as ActorRun
}
/**
* Get dataset interface for reading and filtering data
*
* @param datasetId - Dataset ID from actor run
* @returns ApifyDataset instance
*/
getDataset(datasetId: string): ApifyDataset {
return new ApifyDataset(this.client, datasetId)
}
/**
* Get actor run status
*
* @param runId - Run ID
* @returns Run information
*/
async getRun(runId: string): Promise<ActorRun> {
const run = await this.client.run(runId).get()
return run as ActorRun
}
/**
* Wait for actor run to finish
*
* @param runId - Run ID
* @param options - Wait options
* @returns Final run information
*/
async waitForRun(
runId: string,
options?: {
waitSecs?: number
}
): Promise<ActorRun> {
const run = await this.client.run(runId).waitForFinish({
waitSecs: options?.waitSecs
})
return run as ActorRun
}
}
/**
* Dataset interface for reading and filtering data
*
* KEY FEATURE: Filter data in code BEFORE returning to model context
* This is where the massive token savings happen!
*/
export class ApifyDataset {
constructor(
private client: ApifyClient,
private datasetId: string
) {}
/**
* List dataset items
*
* @param options - List options (pagination, fields)
* @returns Array of dataset items
*/
async listItems(options?: DatasetOptions): Promise<any[]> {
const { items } = await this.client.dataset(this.datasetId).listItems({
offset: options?.offset,
limit: options?.limit,
fields: options?.fields,
omit: options?.omit,
clean: options?.clean
})
return items
}
/**
* Get all dataset items (handles pagination automatically)
*
* WARNING: For large datasets, use listItems with limit
* or filter in code to avoid excessive tokens
*
* @returns Array of all items
*/
async getAllItems(): Promise<any[]> {
const allItems: any[] = []
let offset = 0
const limit = 1000
while (true) {
const { items, count, total } = await this.client.dataset(this.datasetId).listItems({
offset,
limit
})
allItems.push(...items)
if (offset + count >= total) break
offset += limit
}
return allItems
}
/**
* Helper: Filter items by predicate function
*
* This is a convenience method - you can also filter
* using standard array methods after listItems()
*
* @param predicate - Filter function
* @returns Filtered items
*/
async filter(predicate: (item: any) => boolean): Promise<any[]> {
const items = await this.getAllItems()
return items.filter(predicate)
}
/**
* Helper: Get top N items by sort function
*
* @param sortFn - Sort comparison function
* @param limit - Number of items to return
* @returns Top N sorted items
*/
async top(sortFn: (a: any, b: any) => number, limit: number): Promise<any[]> {
const items = await this.getAllItems()
return items.sort(sortFn).slice(0, limit)
}
}
// Re-export for convenience
export { ApifyClient }
Apify Integration Guide
Status: Production Ready ✅ Token Savings: 90-98% vs traditional MCP approach Execution Time: ~10 seconds typical
Integration with PAI Skills
Social Skill Integration
Updated Section: "Fetching Tweet Content"
The social skill now uses code-based Apify scripts instead of mcp__apify MCP tool.
Trigger → Script Mapping:
| User Says | Script to Run |
|---|---|
| "my latest tweet" | get-latest-tweet.ts |
| "my latest thread" | get-latest-thread.ts |
| "get tweets from @user" | get-user-tweets.ts user 5 |
| "what has @user been talking about" | get-user-tweets.ts user 10 |
Example Workflow:
1. User: "Turn my latest tweet into a LinkedIn post" 2. System runs: bun ~/.claude/filesystem-mcps/apify/get-latest-tweet.ts 3. Script returns: Tweet text + metadata (~500 tokens) 4. System transforms tweet into LinkedIn format 5. Token savings: 98% (vs fetching unfiltered profile data)
Research Skill Integration
Use Case: Monitor influential developers' Twitter activity
# Research what ThePrimeagen is discussing
bun ~/.claude/filesystem-mcps/apify/get-user-tweets.ts ThePrimeagen 10
# Analyze Paul Graham's recent thoughts
bun ~/.claude/filesystem-mcps/apify/get-user-tweets.ts paulg 20
# Track Simon Willison's posts
bun ~/.claude/filesystem-mcps/apify/get-user-tweets.ts simonw 15Token Efficiency:
- 10 tweets unfiltered: ~80,000 tokens
- 10 tweets filtered: ~8,000 tokens
- Savings: 90%
Writing Skill Integration
Use Case: Generate blog content from Twitter discussions
# Get user's thread about AI topic
bun ~/.claude/filesystem-mcps/apify/get-latest-thread.ts
# Expand thread into blog post format
# Token efficient: only thread content in contextAvailable Scripts Summary
1. get-latest-tweet.ts
Purpose: User's most recent single tweet Usage: bun get-latest-tweet.ts Returns: Text, date, URL, engagement stats Tokens: ~500
2. get-latest-thread.ts
Purpose: User's most recent Twitter thread Usage: bun get-latest-thread.ts Returns: All thread tweets chronologically Tokens: ~5,500 (for 5-tweet thread) Savings: 87-90% vs unfiltered
3. get-user-tweets.ts
Purpose: Any user's recent tweets Usage: bun get-user-tweets.ts <username> <limit> Returns: Recent tweets with metadata Tokens: ~800 per tweet Savings: 90-95% vs unfiltered
4. debug-tweet-structure.ts
Purpose: Inspect raw API response Usage: bun debug-tweet-structure.ts Returns: Full JSON structure + available fields Use: Development/debugging only
Migration from MCP
Before (MCP Approach)
// Step 1: Search for actors (~1,000 tokens)
mcp__Apify__search-actors("twitter scraper")
// Step 2: Call actor (~1,000 tokens)
mcp__Apify__call-actor(actorId, input)
// Step 3: Get output (~50,000 tokens unfiltered!)
mcp__Apify__get-actor-output(runId)
// Total: ~57,000 tokensAfter (Code-Based Approach)
// All in one script, filtering in code
bun ~/.claude/filesystem-mcps/apify/get-latest-tweet.ts
// Returns only filtered result: ~500 tokens
// Savings: 98.2%Best Practices
DO:
✅ Use appropriate script for the task ✅ Let script filter data before returning ✅ Trust token savings calculations ✅ Run from ~/.claude/filesystem-mcps/apify/ directory or use full path ✅ Check execution time (~10 seconds expected)
DON'T:
❌ Fall back to MCP tools for Twitter operations ❌ Fetch unfiltered data into model context ❌ Re-implement filtering logic (use existing scripts) ❌ Skip error handling (scripts handle common errors) ❌ Ignore token savings metrics in output
Performance Expectations
Execution Time:
- Actor search: Eliminated (hardcoded actor ID)
- Actor execution: ~10 seconds (Apify platform time)
- Data processing: <1 second (TypeScript filtering)
- Total: ~10 seconds
Token Usage:
- Single tweet: 500 tokens (vs 57,000 MCP)
- Thread (5 tweets): 5,500 tokens (vs 60,000 unfiltered)
- User tweets (10): 8,000 tokens (vs 80,000 unfiltered)
Rate Limits:
- Apify free tier: 100 actor runs/day
- Apify paid tier: Unlimited
- Current usage: Well within limits
Error Handling
Scripts handle common errors automatically:
1. Missing APIFY_TOKEN → Clear error message with setup instructions 2. Actor failure → Reports status and exits cleanly 3. No results → Graceful message, no crash 4. Network timeout → Configurable timeout (120s default)
Manual intervention rarely needed.
Future Enhancements
Planned Features:
1. Search tweets by topic
search-tweets.ts <username> <query> <limit>- Example: Search user's tweets about "AI" from last month
2. Thread detection improvements
- Better handling of quote tweets
- Reply chain analysis
- Thread continuity verification
3. Engagement analytics
- Filter by minimum engagement threshold
- Sort by engagement metrics
- Engagement trend analysis
4. Export formats
- JSON output for programmatic use
- Markdown format for documentation
- CSV for spreadsheet analysis
Migration Candidates:
Other Apify actors worth implementing:
- Instagram scraping
- LinkedIn scraping
- YouTube data extraction
- Generic web scraping
Same pattern applies: Filter in code, 90%+ token savings expected.
Documentation
For Users:
- Quick reference:
~/.claude/ - Social skill:
~/.claude/
For Developers:
- Implementation:
~/.claude/ - Standards:
~/.claude/ - Parent guide:
~/.claude/
Support
Common Questions:
Q: Why not use MCP? A: 90-98% token savings, faster execution, better control.
Q: What if script fails? A: Check APIFY_TOKEN in ${PAI_DIR}/.env, verify network, check Apify status.
Q: Can I add new actors? A: Yes! Follow STANDARDS.md pattern, hardcode actor ID, filter in code.
Q: How do I debug? A: Use debug-tweet-structure.ts to inspect raw data, check console output.
Success Metrics
Achieved:
- ✅ 90-98% token reduction vs MCP
- ✅ ~10 second execution time
- ✅ Production integration in social skill
- ✅ 4 production-ready scripts
- ✅ Comprehensive documentation
This is now the standard for all Twitter operations in PAI.
{
"name": "@pai/filesystem-mcp-apify",
"version": "1.0.0",
"type": "module",
"description": "Code-first Apify interface for PAI - replaces token-heavy MCP calls",
"main": "index.ts",
"scripts": {
"example": "bun run examples/instagram-scraper.ts"
},
"dependencies": {
"apify-client": "^2.22.3"
},
"devDependencies": {
"@types/bun": "latest",
"typescript": "^5.0.0"
}
}
Apify Code-First API
Code-based replacement for token-heavy Apify MCP calls.
Progressive disclosure interface for web scraping and automation via the Apify platform. Filter data in code before returning to model context for massive token savings.
Quick Start
import { Apify } from '~/.claude/filesystem-mcps/apify'
const apify = new Apify(process.env.APIFY_TOKEN)
// Search for actors
const actors = await apify.search("instagram scraper")
// Call an actor
const run = await apify.callActor(actors[0].id, {
profiles: ["target"],
resultsLimit: 100
})
// Get and filter results IN CODE (key to token savings!)
const dataset = await apify.getDataset(run.defaultDatasetId)
const items = await dataset.listItems()
// Only filtered results reach model context
const relevant = items
.filter(item => item.likesCount > 1000)
.filter(item => item.timestamp > Date.now() - 86400000)
.slice(0, 10)
console.log(relevant) // Only 10 items vs 100+ unfilteredWhy Code-First?
Token Comparison:
MCP Approach (~57,000 tokens):
1. mcp__Apify__search-actors → 1,000 tokens result
2. mcp__Apify__call-actor → 1,000 tokens result
3. mcp__Apify__get-actor-output → 50,000 tokens unfiltered datasetCode-First (~1,000 tokens - 98.2% reduction):
// All operations in code, filter before returning
const filtered = items.filter(...).slice(0, 10)
// Only 10 filtered items (500 tokens) reach modelCore API
Apify Class
Main client for interacting with Apify platform.
Constructor:
new Apify(token?: string)token- Apify API token (defaults toprocess.env.APIFY_TOKEN)
Methods:
search(query, options?)
Search for actors by keyword.
const actors = await apify.search("instagram scraper", {
limit: 10,
offset: 0
})Parameters:
query- Search keywordsoptions.limit- Max results (default: 10)options.offset- Skip results (default: 0)
Returns: Array of Actor objects with id, name, title, description, stats
callActor(actorId, input, options?)
Execute an actor.
const run = await apify.callActor("apify/instagram-scraper", {
profiles: ["target"],
resultsLimit: 100
}, {
memory: 2048,
timeout: 300
})Parameters:
actorId- Actor ID or "username/actor-name"input- Actor-specific input configurationoptions.memory- Memory in MB (128, 256, 512, 1024, 2048, etc.)options.timeout- Timeout in secondsoptions.build- Build number or tag
Returns: ActorRun object with run details and defaultDatasetId
getDataset(datasetId)
Get dataset interface for reading/filtering data.
const dataset = await apify.getDataset(run.defaultDatasetId)Returns: ApifyDataset instance
getRun(actorId, runId)
Get run status.
const run = await apify.getRun(actorId, runId)Returns: ActorRun object with current status
waitForRun(actorId, runId, options?)
Wait for run to finish.
const finalRun = await apify.waitForRun(actorId, runId, {
waitSecs: 120
})Returns: Final ActorRun object when complete
ApifyDataset Class
Interface for reading and filtering dataset results.
Key Concept: Filter in code BEFORE returning to model context!
Methods:
listItems(options?)
List dataset items with pagination.
const items = await dataset.listItems({
offset: 0,
limit: 100,
fields: ['username', 'likesCount', 'text']
})Parameters:
options.offset- Skip itemsoptions.limit- Max itemsoptions.fields- Include only these fieldsoptions.omit- Exclude these fieldsoptions.clean- Clean HTML/special chars
Returns: Array of dataset items
getAllItems()
Get all items (handles pagination automatically).
Warning: For large datasets, use listItems() with limit or filter in code.
const allItems = await dataset.getAllItems()
const filtered = allItems.filter(item => item.score > 0.8)Returns: Array of all dataset items
filter(predicate)
Helper to filter items by predicate.
const relevant = await dataset.filter(item =>
item.likesCount > 1000 &&
item.timestamp > Date.now() - 86400000
)Parameters:
predicate- Filter function(item) => boolean
Returns: Filtered items array
top(sortFn, limit)
Helper to get top N items by sort function.
const topPosts = await dataset.top(
(a, b) => b.likesCount - a.likesCount,
10
)Parameters:
sortFn- Sort comparison functionlimit- Number of items to return
Returns: Top N sorted items
Common Patterns
Pattern 1: Search → Call → Filter Results
// Find actor
const actors = await apify.search("web scraper")
const actor = actors[0]
// Execute actor
const run = await apify.callActor(actor.id, {
startUrls: ["https://example.com"],
maxPages: 50
})
// Wait for completion
await apify.waitForRun(actor.id, run.id)
// Get and filter results
const dataset = apify.getDataset(run.defaultDatasetId)
const items = await dataset.listItems({ limit: 100 })
// Filter in code - only relevant items reach model
const relevant = items
.filter(item => item.price < 100)
.filter(item => item.inStock)
.slice(0, 10)Pattern 2: Process Large Dataset in Chunks
const dataset = apify.getDataset(datasetId)
// Process in batches to avoid memory issues
let offset = 0
const limit = 1000
const results = []
while (true) {
const batch = await dataset.listItems({ offset, limit })
if (batch.length === 0) break
// Filter each batch
const filtered = batch.filter(item => item.relevant === true)
results.push(...filtered)
offset += limit
}
// Only filtered results go to model context
console.log(results)Pattern 3: Get Top Performers
const dataset = apify.getDataset(datasetId)
// Get top 10 posts by engagement
const topPosts = await dataset.top(
(a, b) => b.likesCount - a.likesCount,
10
)
// Only top 10 items (not entire dataset) reach model
console.log(topPosts)Environment Variables
# Required
APIFY_TOKEN=apify_api_xxxxx...
# Optional (uses defaults if not set)
APIFY_API_BASE_URL=https://api.apify.com/v2Get your token from: https://console.apify.com/account/integrations
TypeScript Types
All types are exported from the main module:
import { Actor, ActorRun, DatasetOptions } from '~/.claude/filesystem-mcps/apify'Error Handling
try {
const run = await apify.callActor(actorId, input)
await apify.waitForRun(actorId, run.id)
const finalRun = await apify.getRun(actorId, run.id)
if (finalRun.status !== 'SUCCEEDED') {
console.error('Actor run failed:', finalRun.status)
return
}
// Process results...
} catch (error) {
console.error('Apify error:', error.message)
}Running Examples
# Run the Instagram scraper example
cd ~/.claude/filesystem-mcps/apify
bun run examples/instagram-scraper.ts
# Or use bun directly
bun examples/instagram-scraper.tsToken Savings Calculator
Estimate your token savings:
function estimateTokens(data: any): number {
const str = JSON.stringify(data)
return Math.ceil(str.length / 4) // ~4 chars per token
}
// Before (MCP)
const allItems = await dataset.getAllItems() // 10,000 items
console.log('MCP tokens:', estimateTokens(allItems)) // ~50,000
// After (Code-First)
const filtered = allItems.filter(...).slice(0, 10) // 10 items
console.log('Code tokens:', estimateTokens(filtered)) // ~500
// Savings: 99% token reduction!When to Use Code-First vs MCP
Use Code-First (this API):
- ✅ Need to filter/transform large datasets
- ✅ Processing 100+ results and want top 10
- ✅ Multiple operations in sequence (search → call → filter)
- ✅ Control flow (loops, conditionals)
- ✅ Privacy-sensitive data that shouldn't enter model context
Use MCP:
- ❌ Simple single operations with small results
- ❌ Need to expose to non-code-capable models
- ❌ Provider-specific features not in this wrapper
Links
- Apify Platform: https://apify.com
- Apify Console: https://console.apify.com
- Actor Store: https://apify.com/store
- API Docs: https://docs.apify.com/api/v2
- Parent README:
~/.claude/
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "esnext",
"target": "esnext",
"moduleResolution": "bundler",
"moduleDetection": "force",
"allowImportingTsExtensions": true,
"noEmit": true,
"composite": true,
"strict": true,
"downlevelIteration": true,
"skipLibCheck": true,
"jsx": "react-jsx",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"types": [
"bun-types"
]
}
}
/**
* Common types shared across all Apify actors
*/
/**
* Standard pagination options for all scrapers
*/
export interface PaginationOptions {
/** Maximum number of results to return */
maxResults?: number
/** Skip first N results */
offset?: number
}
/**
* Date range filter options
*/
export interface DateRangeOptions {
/** Start date (ISO string or Date object) */
from?: string | Date
/** End date (ISO string or Date object) */
to?: string | Date
}
/**
* Engagement metrics common to social media posts
*/
export interface EngagementMetrics {
likesCount?: number
commentsCount?: number
sharesCount?: number
viewsCount?: number
}
/**
* Standard user/profile information
*/
export interface UserProfile {
id?: string
username?: string
fullName?: string
bio?: string
profilePictureUrl?: string
followersCount?: number
followingCount?: number
verified?: boolean
}
/**
* Standard post/content structure
*/
export interface Post extends EngagementMetrics {
id: string
url: string
text?: string
caption?: string
timestamp: string
author?: UserProfile
imageUrls?: string[]
videoUrl?: string
hashtags?: string[]
mentions?: string[]
}
/**
* Geo-location data
*/
export interface Location {
latitude?: number
longitude?: number
address?: string
city?: string
state?: string
country?: string
postalCode?: string
}
/**
* Contact information
*/
export interface ContactInfo {
email?: string
phone?: string
website?: string
socialMedia?: {
facebook?: string
twitter?: string
instagram?: string
linkedin?: string
youtube?: string
}
}
/**
* Business/place information
*/
export interface BusinessInfo {
name: string
category?: string
rating?: number
reviewsCount?: number
priceLevel?: number
location?: Location
contact?: ContactInfo
openingHours?: string[]
isOpen?: boolean
}
/**
* Actor run options for controlling execution
*/
export interface ActorRunOptions {
/** Memory allocation in MB (128, 256, 512, 1024, 2048, 4096, 8192) */
memory?: number
/** Timeout in seconds */
timeout?: number
/** Build tag or number to use */
build?: string
}
/**
* Error result when actor fails
*/
export interface ActorError {
message: string
actorId: string
runId?: string
status?: string
}
/**
* Type definitions for Apify actors
*/
export * from './common'