Scrape Google Maps for local businesses by category and location, output CSV ready for cold email enrichment.

MITAuto-check: notesSales & Support

Install Google Maps List Builder

skills CLI
$ npx skills add growthenginenowoslawski/coldoutboundskills --skill google-maps-list-builder -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install growthenginenowoslawski/coldoutboundskills google-maps-list-builder --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/growthenginenowoslawski/coldoutboundskills.git skills-src && mkdir -p .claude/skills && cp -r skills-src/skills/google-maps-list-builder .claude/skills/google-maps-list-builder && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
google-maps-list-builder
GitHub stars
753
Token cost
~6.2k tokens
SKILL.md length
920 words
Files
1
Skills in repo
49
Repo updated
First seen
Licence
MIT

At a glance

Scrape Google Maps for local businesses by category and location, output CSV ready for cold email enrichment.

  • Works in 5 steps: Run this skill → CSV of businesses with… → Run /icp-prompt-builder on a sample of… → Run /blitz-list-builder with the… → …
  • Tasks that involve Cold outreach
  • SKILL.md covers How this fits in the cold…, Required step: Qualify with…, What You Need Before Starting and Project Setup, plus 9 more sections
  • Calls npm, npx and fly; reaches rapidapi.com; needs RAPIDAPI_KEY

What it does

Google Maps List Builder is an agent skill from growthenginenowoslawski/coldoutboundskills. Scrape Google Maps for local businesses by category and location, output CSV ready for cold email enrichment. Best for SMB campaigns targeting restaurants, clinics, gyms, salons, contractors, etc. Uses RapidAPI Maps Data API. Output feeds directly into /blitz-list-builder (to find owner contacts) or //list-builder Phase 5 (if you have names already).

Its SKILL.md is about 6.2k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.

It sits in Sales & Support, covering Cold outreach, Web scraping and CSV and tabular files. It works with Google Maps Platform. The repository describes itself as: Open-source Claude Code skills for cold email and outbound sales. Grade campaigns, export Prospeo searches, scrape Google Maps — all from Claude Code. The licence is MIT.

When your agent uses it

  • Tasks that involve Cold outreach
  • Tasks that involve Web scraping
  • Tasks that involve CSV and tabular files

Example prompts

  • “/google-maps-list-builder”

Requirements

  • Node.js
  • A credential in RAPIDAPI_KEY

Workflow steps

5 steps, taken from the first numbered list in SKILL.md.

  1. Run this skill → CSV of businesses with company_domain
  2. Run /icp-prompt-builder on a sample of 50 — tune a qualification prompt to filter out bad fits before paying for downstream enrichment
  3. Run /blitz-list-builder with the filtered CSV → adds owners/managers to each business
  4. Run /list-builder (Phase 5, emails) → fills in missing emails
  5. Run /cold-email-starter-kit's smartlead-add-leads.ts → upload to Smartlead

What it can do on your machine

Read from SKILL.md and the folder at commit 25c5d85. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • npm
    • npx
    • fly

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    Hosts in commands or code, which the agent is likely to contact:

    • rapidapi.com

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names these keys or tokens, usually read from environment variables:

    • RAPIDAPI_KEY

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Google Maps List Builder loads about 6.2k tokens when it runs. Until then it costs about 94 tokens; SKILL.md has 920 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~94
When it runs · the whole SKILL.md, loaded when a task matches
~6.2k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check: notes

The automated check noted patterns worth knowing about, such as sudo or a known installer.

  • NoteMentions a .env fileSKILL.md:88
    Or create a `.env` file (add `.env` to `.gitignore`):

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from growthenginenowoslawski/coldoutboundskills at commit 25c5d85, republished under its MIT licence (© growthenginenowoslawski). 920 words, ~6,160 tokens.

Download SKILL.mdSave it as .claude/skills/google-maps-list-builder/SKILL.md (or your agent's skills folder).
name
google-maps-list-builder
description
Scrape Google Maps for local businesses by category and location, output CSV ready for cold email enrichment. Best for SMB campaigns targeting restaurants, clinics, gyms, salons, contractors, etc. Uses RapidAPI Maps Data API. Output feeds directly into /blitz-list-builder (to find owner contacts) or //list-builder Phase 5 (if you have names already).

Google Maps List Builder

A self-contained tool for scraping business listings from Google Maps. Give it a search query (e.g., "pizza restaurant") and a location (zip code, city, or coordinates), and it returns structured data for every matching business — written to CSV.

How this fits in the cold email flow

Google Maps gives you COMPANIES (name, domain, phone, address, ratings). It does NOT give you PEOPLE. To run cold email:

  1. Run this skill → CSV of businesses with company_domain
  2. Run /icp-prompt-builder on a sample of 50 — tune a qualification prompt to filter out bad fits before paying for downstream enrichment
  3. Run /blitz-list-builder with the filtered CSV → adds owners/managers to each business
  4. Run /list-builder (Phase 5, emails) → fills in missing emails
  5. Run /cold-email-starter-kit's smartlead-add-leads.ts → upload to Smartlead

This skill is only the first step.

Required step: Qualify with /icp-prompt-builder

This is a required step. Do not skip it.

Google Maps will happily return 10,000 "pizza restaurants in Illinois," but most of those won't match your actual ICP (maybe you only want 50-200 seat operators, or only ones without online ordering). Before spending on enrichment, sample ~50 results and run /icp-prompt-builder:

  1. Evaluate 10 results with an AI qualification prompt
  2. You flag "this one should be NO, they're a chain franchise"
  3. Refine, run next 10
  4. Stop when 2 rounds show no corrections
  5. Apply tuned prompt to filter the rest of the scrape

Why required: downstream owner-finding (via /blitz-list-builder) and email waterfall cost $0.10-$0.30 per contact. On a 10,000-business scrape, that's $1K-$3K. Qualifying upfront saves 50-80% of that spend on average.

What You Need Before Starting

  1. Node.js 18+ and npm installed
  2. A RapidAPI account (free tier available) with a subscription to the Maps Data API:

That's it. No Google Cloud account, no OAuth, no billing setup beyond RapidAPI.

Project Setup

Create a new project directory and initialize it:

bash
mkdir google-maps-scraper && cd google-maps-scraper
npm init -y
npm install typescript bottleneck
npm install -D @types/node tsx

Add to package.json scripts:

json
{
  "scripts": {
    "scrape": "tsx src/index.ts"
  }
}

Create tsconfig.json:

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "strict": true,
    "outDir": "dist",
    "rootDir": "src",
    "skipLibCheck": true
  },
  "include": ["src"]
}

Set your API key as an environment variable:

bash
export RAPIDAPI_KEY=your_key_here

Or create a .env file (add .env to .gitignore):

RAPIDAPI_KEY=your_key_here

File Structure

google-maps-scraper/
  data/
    us-zip-codes.csv   # 42,734 US zip codes with city, state, lat/lng, population
  src/
    index.ts           # CLI entry point
    client.ts          # RapidAPI Maps Data client with rate limiting
    types.ts           # TypeScript interfaces
    csv.ts             # CSV export
    zips.ts            # Zip code loader (filter by state, city, population)

Bundled Zip Code Database

The repo includes data/us-zip-codes.csv — a complete US zip code reference with 42,734 entries. Columns:

zip,primary_city,state,timezone,area_codes,world_region,country,latitude,longitude,irs_estimated_population

This lets you scrape an entire state or metro area without manually listing zip codes. The src/zips.ts loader provides filtering by state, city, and minimum population.

Core Files

src/types.ts
typescript
export interface SearchParams {
  query: string;       // "pizza restaurant", "dentist", "gym"
  lat?: number;        // Center latitude (optional if using "query in zipcode" format)
  lng?: number;        // Center longitude
  limit?: number;      // Max results per search (default 20, max 20)
  zoom?: number;       // Map zoom level (default 13 = neighborhood)
  country?: string;    // Country code (default "us")
}

export interface Place {
  place_id: string;
  name: string;
  address: string;
  lat: number;
  lng: number;
  rating?: number;
  reviews_count?: number;
  phone?: string;
  website?: string;
  types?: string[];
  category?: string;
}

export interface ScrapeResult {
  query: string;
  location: string;
  total_results: number;
  unique_results: number;
  places: Place[];
  duration_ms: number;
}
src/zips.ts

Loads and filters the bundled zip code CSV. Lets you target by state, city name, or minimum population.

typescript
import { readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';

export interface ZipEntry {
  zip: string;
  city: string;
  state: string;
  lat: number;
  lng: number;
  population: number;
}

let cache: ZipEntry[] | null = null;

function loadAll(): ZipEntry[] {
  if (cache) return cache;
  const __dirname = dirname(fileURLToPath(import.meta.url));
  const csvPath = join(__dirname, '..', 'data', 'us-zip-codes.csv');
  const raw = readFileSync(csvPath, 'utf-8');
  const lines = raw.trim().split('\n').slice(1); // skip header

  cache = lines.map(line => {
    // Handle quoted fields (area_codes can contain commas)
    const parts: string[] = [];
    let current = '';
    let inQuotes = false;
    for (const ch of line) {
      if (ch === '"') { inQuotes = !inQuotes; continue; }
      if (ch === ',' && !inQuotes) { parts.push(current); current = ''; continue; }
      current += ch;
    }
    parts.push(current);

    return {
      zip: parts[0]?.padStart(5, '0') || '',
      city: parts[1] || '',
      state: parts[2] || '',
      lat: parseFloat(parts[7]) || 0,
      lng: parseFloat(parts[8]) || 0,
      population: parseInt(parts[9]) || 0,
    };
  }).filter(z => z.zip.length === 5);

  return cache;
}

/** Get zips for a US state (2-letter code, e.g. "CA", "TX") */
export function getZipsByState(stateCode: string): ZipEntry[] {
  return loadAll().filter(z => z.state.toUpperCase() === stateCode.toUpperCase());
}

/** Get zips for a city name (case-insensitive, partial match) */
export function getZipsByCity(city: string, state?: string): ZipEntry[] {
  const cityLower = city.toLowerCase();
  return loadAll().filter(z => {
    const cityMatch = z.city.toLowerCase().includes(cityLower);
    const stateMatch = !state || z.state.toUpperCase() === state.toUpperCase();
    return cityMatch && stateMatch;
  });
}

/** Get zips with population above a threshold */
export function getZipsByMinPopulation(minPop: number, state?: string): ZipEntry[] {
  return loadAll().filter(z => {
    const popMatch = z.population >= minPop;
    const stateMatch = !state || z.state.toUpperCase() === state.toUpperCase();
    return popMatch && stateMatch;
  });
}

/** Get all loaded zip entries */
export function getAllZips(): ZipEntry[] {
  return loadAll();
}
src/client.ts

This is the core API client. It handles rate limiting (2 req/sec) and retries with exponential backoff.

typescript
import Bottleneck from 'bottleneck';
import type { SearchParams, Place } from './types.js';

interface RawSearchResponse {
  data?: Array<{
    place_id?: string;
    title?: string;
    name?: string;
    address?: string;
    latitude?: number;
    longitude?: number;
    rating?: number;
    reviews?: number;
    phone?: string;
    website?: string;
    types?: string[];
    type?: string;
    category?: string;
  }>;
  error?: string;
}

interface GeocodingResponse {
  latitude?: number;
  longitude?: number;
  formatted_address?: string;
  error?: string;
}

export class GoogleMapsClient {
  private limiter: Bottleneck;
  private apiKey: string;
  private host = 'maps-data.p.rapidapi.com';
  private maxRetries: number;

  constructor(opts: { apiKey: string; requestsPerSecond?: number; maxRetries?: number }) {
    this.apiKey = opts.apiKey;
    this.maxRetries = opts.maxRetries ?? 3;
    this.limiter = new Bottleneck({
      maxConcurrent: 1,
      minTime: Math.floor(1000 / (opts.requestsPerSecond ?? 2)),
    });
  }

  /** Search Google Maps for businesses */
  async search(params: SearchParams): Promise<Place[]> {
    const response = await this.request<RawSearchResponse>('searchmaps.php', {
      query: params.query,
      limit: String(params.limit ?? 20),
      country: params.country ?? 'us',
      ...(params.lat != null && { lat: String(params.lat) }),
      ...(params.lng != null && { lng: String(params.lng) }),
      ...(params.zoom != null && { zoom: String(params.zoom) }),
    });

    if (response.error) throw new Error(`Search failed: ${response.error}`);
    return this.transform(response.data || []);
  }

  /** Geocode a zip code or address to lat/lng */
  async geocode(query: string, country = 'us'): Promise<{ lat: number; lng: number }> {
    const response = await this.request<GeocodingResponse>('geocoding.php', {
      query: `${query}, ${country.toUpperCase()}`,
    });
    if (!response.latitude || !response.longitude) {
      throw new Error(`Could not geocode: ${query}`);
    }
    return { lat: response.latitude, lng: response.longitude };
  }

  private async request<T>(endpoint: string, params: Record<string, string>): Promise<T> {
    return this.limiter.schedule(() => this.requestWithRetry<T>(endpoint, params));
  }

  private async requestWithRetry<T>(
    endpoint: string,
    params: Record<string, string>,
    attempt = 0
  ): Promise<T> {
    const url = new URL(`https://${this.host}/${endpoint}`);
    for (const [k, v] of Object.entries(params)) {
      if (v != null) url.searchParams.set(k, v);
    }

    try {
      const res = await fetch(url.toString(), {
        headers: {
          'X-RapidAPI-Key': this.apiKey,
          'X-RapidAPI-Host': this.host,
        },
      });

      if (!res.ok) {
        const err: any = new Error(`API ${res.status}: ${res.statusText}`);
        err.statusCode = res.status;
        throw err;
      }

      return (await res.json()) as T;
    } catch (err: any) {
      const retryable =
        attempt < this.maxRetries &&
        (err.statusCode === 429 || err.statusCode >= 500 ||
         err.code === 'ECONNRESET' || err.code === 'ETIMEDOUT');

      if (retryable) {
        const delay = 1000 * Math.pow(2, attempt);
        console.log(`  Retry ${attempt + 1}/${this.maxRetries} in ${delay}ms...`);
        await new Promise(r => setTimeout(r, delay));
        return this.requestWithRetry<T>(endpoint, params, attempt + 1);
      }
      throw err;
    }
  }

  private transform(data: NonNullable<RawSearchResponse['data']>): Place[] {
    return data.map(item => ({
      place_id: item.place_id || '',
      name: item.title || item.name || '',
      address: item.address || '',
      lat: item.latitude || 0,
      lng: item.longitude || 0,
      rating: item.rating,
      reviews_count: item.reviews,
      phone: item.phone,
      website: item.website,
      types: item.types || (item.type ? [item.type] : []),
      category: item.category || item.type,
    }));
  }
}
src/csv.ts
typescript
import { writeFile, mkdir } from 'fs/promises';
import { dirname } from 'path';
import type { Place } from './types.js';

const HEADERS = [
  'place_id', 'name', 'address', 'phone', 'website',
  'rating', 'reviews_count', 'lat', 'lng', 'category',
];

function escape(val: string | number | undefined | null): string {
  if (val == null) return '';
  const s = String(val);
  return s.includes(',') || s.includes('"') || s.includes('\n')
    ? `"${s.replace(/"/g, '""')}"`
    : s;
}

export async function exportCSV(places: Place[], outputPath: string): Promise<void> {
  await mkdir(dirname(outputPath), { recursive: true });
  const lines = [
    HEADERS.join(','),
    ...places.map(p =>
      HEADERS.map(h => escape(p[h as keyof Place])).join(',')
    ),
  ];
  await writeFile(outputPath, lines.join('\n') + '\n', 'utf-8');
}
src/index.ts
typescript
import { GoogleMapsClient } from './client.js';
import { exportCSV } from './csv.js';
import { getZipsByState, getZipsByCity, getZipsByMinPopulation } from './zips.js';
import type { Place } from './types.js';

function dedup(places: Place[]): Place[] {
  const seen = new Set<string>();
  return places.filter(p => {
    if (seen.has(p.place_id)) return false;
    seen.add(p.place_id);
    return true;
  });
}

function getArg(args: string[], prefix: string): string | undefined {
  const match = args.find(a => a.startsWith(prefix));
  return match ? match.split('=').slice(1).join('=') : undefined;
}

async function main() {
  const args = process.argv.slice(2);

  const query = getArg(args, '--query=');
  const zips = getArg(args, '--zips=');
  const cities = getArg(args, '--cities=');
  const state = getArg(args, '--state=');
  const minPop = getArg(args, '--min-pop=');
  const limit = parseInt(getArg(args, '--limit=') || '20', 10);
  const output = getArg(args, '--output=') || './output/results.csv';

  if (!query || (!zips && !cities && !state)) {
    console.log(`
Google Maps Scraper

USAGE:
  npm run scrape -- --query="pizza restaurant" --zips=10014,10013,10012
  npm run scrape -- --query="dentist" --state=TX
  npm run scrape -- --query="dentist" --state=TX --min-pop=10000
  npm run scrape -- --query="gym" --cities="Austin TX,Dallas TX"

OPTIONS:
  --query=QUERY      Business type to search for (required)
  --zips=ZIP,ZIP     Comma-separated zip codes to search
  --cities=CITY,CITY Comma-separated cities to search
  --state=XX         Search all zip codes in a US state (2-letter code)
  --min-pop=N        Filter zips to those with population >= N (use with --state)
  --limit=N          Max results per location (default 20, max 20)
  --output=PATH      Output CSV path (default ./output/results.csv)

ENVIRONMENT:
  RAPIDAPI_KEY       Your RapidAPI key (required)
                     Get one at: https://rapidapi.com/alexanderxbx/api/maps-data

EXAMPLES:
  # All pizza places in California (zips with pop >= 5000)
  npm run scrape -- --query="pizza restaurant" --state=CA --min-pop=5000

  # Dentists in specific NYC zip codes
  npm run scrape -- --query="dentist" --zips=10014,10013,10012

  # Gyms across Texas cities
  npm run scrape -- --query="gym" --cities="Austin TX,Dallas TX,Houston TX"
`);
    process.exit(1);
  }

  const apiKey = process.env.RAPIDAPI_KEY;
  if (!apiKey) {
    console.error('Error: RAPIDAPI_KEY environment variable is required');
    console.error('Get your key at: https://rapidapi.com/alexanderxbx/api/maps-data');
    process.exit(1);
  }

  // Build location list from all sources
  const locations: string[] = [];

  if (zips) {
    locations.push(...zips.split(',').map(s => s.trim()));
  }
  if (cities) {
    locations.push(...cities.split(',').map(s => s.trim()));
  }
  if (state) {
    const minPopNum = minPop ? parseInt(minPop, 10) : 0;
    const stateZips = minPopNum > 0
      ? getZipsByMinPopulation(minPopNum, state)
      : getZipsByState(state);
    locations.push(...stateZips.map(z => z.zip));
    console.log(`Loaded ${stateZips.length} zip codes for ${state.toUpperCase()}${minPopNum > 0 ? ` (pop >= ${minPopNum.toLocaleString()})` : ''}`);
  }

  if (locations.length === 0) {
    console.error('No locations to search. Provide --zips, --cities, or --state.');
    process.exit(1);
  }

  console.log(`Scraping "${query}" across ${locations.length} location(s)...\n`);

  const client = new GoogleMapsClient({ apiKey, requestsPerSecond: 2 });
  const allPlaces: Place[] = [];
  const start = Date.now();

  for (let i = 0; i < locations.length; i++) {
    const loc = locations[i];
    console.log(`  [${i + 1}/${locations.length}] Searching: ${query} in ${loc}`);

    try {
      const results = await client.search({
        query: `${query} in ${loc}`,
        limit,
        country: 'us',
      });
      allPlaces.push(...results);
      console.log(`    Found ${results.length} results`);
    } catch (err: any) {
      console.error(`    Error: ${err.message}`);
    }
  }

  // Deduplicate
  const unique = dedup(allPlaces);
  console.log(`\nTotal: ${allPlaces.length} results, ${unique.length} unique after dedup`);

  // Export
  await exportCSV(unique, output);
  console.log(`Saved to: ${output}`);

  const duration = ((Date.now() - start) / 1000).toFixed(1);
  console.log(`Done in ${duration}s`);

  // Print sample
  if (unique.length > 0) {
    console.log('\nSample result:');
    const sample = unique[0];
    console.log(`  ${sample.name}`);
    console.log(`  ${sample.address}`);
    if (sample.phone) console.log(`  ${sample.phone}`);
    if (sample.website) console.log(`  ${sample.website}`);
    if (sample.rating) console.log(`  ${sample.rating} stars (${sample.reviews_count} reviews)`);
  }
}

main().catch(err => {
  console.error('Fatal:', err.message);
  process.exit(1);
});

Running It

Local CLI
bash
# Search pizza places in 3 NYC zip codes
npm run scrape -- --query="pizza restaurant" --zips=10014,10013,10012

# Search ALL dentists in Texas (zips with population >= 5000)
npm run scrape -- --query="dentist" --state=TX --min-pop=5000

# Every gym in California (all 2,657 zip codes — takes a while)
npm run scrape -- --query="gym" --state=CA

# Search dentists across specific cities
npm run scrape -- --query="dentist" --cities="Austin TX,Dallas TX,San Antonio TX"

# Custom output file
npm run scrape -- --query="gym" --zips=90210 --output=./data/gyms.csv
Programmatic Usage
typescript
import { GoogleMapsClient } from './client.js';

const client = new GoogleMapsClient({
  apiKey: process.env.RAPIDAPI_KEY!,
  requestsPerSecond: 2,
});

// Simple search
const places = await client.search({
  query: 'coffee shop in 94105',
  limit: 20,
});

// Search with coordinates
const { lat, lng } = await client.geocode('94105');
const nearby = await client.search({
  query: 'coffee shop',
  lat,
  lng,
  zoom: 14,
  limit: 20,
});

Deploying as a Web App (Optional)

If you want a browser UI instead of (or in addition to) the CLI, add Express:

bash
npm install express
npm install -D @types/express

Create src/server.ts:

typescript
import express from 'express';
import { GoogleMapsClient } from './client.js';
import { exportCSV } from './csv.js';
import { tmpdir } from 'os';
import { join } from 'path';
import { readFile, unlink } from 'fs/promises';

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

const client = new GoogleMapsClient({
  apiKey: process.env.RAPIDAPI_KEY!,
  requestsPerSecond: 2,
});

// Simple HTML form
app.get('/', (_req, res) => {
  res.send(`<!DOCTYPE html>
<html><head><title>Google Maps Scraper</title></head>
<body style="font-family:sans-serif;max-width:600px;margin:40px auto;padding:0 20px">
  <h1>Google Maps Scraper</h1>
  <form method="POST" action="/scrape">
    <label>Search query:<br>
      <input name="query" placeholder="pizza restaurant" style="width:100%;padding:8px;margin:4px 0 12px" required>
    </label>
    <label>Locations (comma-separated zips or cities):<br>
      <input name="locations" placeholder="10014, 10013, 10012" style="width:100%;padding:8px;margin:4px 0 12px" required>
    </label>
    <button type="submit" style="padding:10px 24px;cursor:pointer">Scrape</button>
  </form>
</body></html>`);
});

app.post('/scrape', async (req, res) => {
  const { query, locations: locStr } = req.body;
  const locations = locStr.split(',').map((s: string) => s.trim()).filter(Boolean);

  const allPlaces: any[] = [];
  for (const loc of locations) {
    try {
      const results = await client.search({ query: `${query} in ${loc}`, limit: 20 });
      allPlaces.push(...results);
    } catch {}
  }

  // Dedup
  const seen = new Set<string>();
  const unique = allPlaces.filter(p => { if (seen.has(p.place_id)) return false; seen.add(p.place_id); return true; });

  // Export CSV and send as download
  const tmpPath = join(tmpdir(), `scrape-${Date.now()}.csv`);
  await exportCSV(unique, tmpPath);
  const csv = await readFile(tmpPath, 'utf-8');
  await unlink(tmpPath);

  res.setHeader('Content-Type', 'text/csv');
  res.setHeader('Content-Disposition', `attachment; filename="maps-scrape-${Date.now()}.csv"`);
  res.send(csv);
});

const port = parseInt(process.env.PORT || '3000', 10);
app.listen(port, () => console.log(`Scraper running at http://localhost:${port}`));

Add a script to package.json:

json
{
  "scripts": {
    "scrape": "tsx src/index.ts",
    "serve": "tsx src/server.ts"
  }
}

Run locally: npm run serve then open http://localhost:3000

Deploying to Railway
  1. Push your project to a GitHub repo
  2. Go to https://railway.com, create a new project, connect the repo
  3. Set the environment variable RAPIDAPI_KEY in Railway's dashboard
  4. Set the start command to npx tsx src/server.ts
  5. Railway auto-detects the port from process.env.PORT and gives you a public URL

You can add password protection by checking a PASSWORD env var in the POST handler, or use Railway's built-in auth features.

Show full SKILL.md (352 more words)Show less
Deploying to Other Platforms

This is a standard Node.js app. It runs anywhere:

  • Render: Connect GitHub repo, set env vars, done
  • Fly.io: fly launch, set secrets with fly secrets set RAPIDAPI_KEY=xxx
  • Vercel: Deploy as a serverless function (modify server.ts to export handlers)
  • Docker: FROM node:20-slim + npm install + npx tsx src/server.ts

API Reference

The underlying API is the Maps Data API on RapidAPI: https://rapidapi.com/alexanderxbx/api/maps-data

Key Endpoints Used
EndpointPurposeExample
searchmaps.phpSearch businesses by query + location?query=pizza+in+10014&limit=20
geocoding.phpConvert address/zip to lat/lng?query=10014,+US
nearby.phpSearch near a lat/lng point?query=pizza&lat=40.73&lng=-74.00
place.phpGet full details for one business?place_id=ChIJ...
Rate Limits

The free tier on RapidAPI has request limits (check your plan). The client is hard-coded to 2 requests/second with automatic retries on 429s. Adjust requestsPerSecond if your plan allows more.

Response Fields

Each result includes:

  • place_id — unique Google Maps identifier
  • name — business name
  • address — full street address
  • phone — phone number (if listed)
  • website — website URL (if listed)
  • rating — star rating (1-5)
  • reviews_count — number of Google reviews
  • lat / lng — coordinates
  • types / category — business categories (e.g., "pizza_restaurant")

Tips

  • "query in zipcode" format works best for US searches. No coordinates needed.
  • 20 results per search is the max. To get more coverage, search multiple overlapping zip codes.
  • Dedup by place_id — the same business often shows up in adjacent zip code searches.
  • Cuisine/category filtering: The types field tells you what kind of business it is. Use it to filter out irrelevant results (e.g., filter out "bar" when searching for "restaurant").
  • Cost: Check your RapidAPI plan. The free tier usually gives you enough for testing. Paid plans are cheap for bulk scraping.

What to do next

Run /icp-prompt-builder on a 50-business sample (required step above). Then /blitz-list-builder with the filtered domains to find owner contacts — Google Maps returns businesses, not people.

After owner discovery: /list-builder (Phase 5, emails) to fill missing emails, then /list-quality-scorecard to grade.

Or wait: if your scrape returned <200 businesses, your query + location is too narrow. Widen before proceeding.

  • /icp-prompt-builder — required qualification pass
  • /blitz-list-builder — find owner contacts at each business
  • /list-builder (Phase 5, emails) — fill missing emails
  • /list-quality-scorecard — grade the final list

© growthenginenowoslawski, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in skills/google-maps-list-builder of growthenginenowoslawski/coldoutboundskills.

Open the folder on GitHubat commit 25c5d85

Compare with similar skills

Google Maps List Builder next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Google Maps List Builder compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Google Maps List Builder this skillgrowthenginenowoslawski/coldoutboundskills753—~6.2kAutomated safety check: NotesMIT
Cold Email Local Businessgmapsscraper/google-maps-agent-skills132—~1.2kAutomated safety check: PassMIT
Google Maps Reviews Scrapergmapsscraper/google-maps-agent-skills132—~1.2kAutomated safety check: PassMIT
Apify Buying Signal Detectionapify/awesome-skills266—~5.1kAutomated safety check: NotesApache-2.0
Google Maps API Skillbrowser-act/skills6.1k1 repos~1.4kAutomated safety check: PassMIT
Business Email Extractorgmapsscraper/google-maps-agent-skills132—~1.1kAutomated safety check: PassMIT

Similar skills

  • Cold Email Local Business

    gmapsscraper/google-maps-agent-skills

    Write hyper-personalized cold emails to local businesses using their Google Maps data (CSV).

    132 GitHub stars~1.2k tokensUpdated 4 mo ago
    Sales & SupportAuto-check passed
  • Google Maps Reviews Scraper

    gmapsscraper/google-maps-agent-skills

    Analyze Google Maps review data from CSV exports. An agent skill from gmapsscraper/google-maps-agent-skills.

    132 GitHub stars~1.2k tokensUpdated 4 mo ago
    Sales & SupportAuto-check passed
  • Apify Buying Signal Detection

    apify/awesome-skills

    Official

    Set up a recurring buying-signal detection pipeline that finds companies showing buying intent across three signal types — job postings (hiring for the persona), fundraising events (recent raises)…

    266 GitHub stars~5.1k tokensUpdated 18 days ago
    Business, Finance & HRAuto-check: notes
  • Google Maps API Skill

    browser-act/skills

    This skill helps users automatically scrape business data from Google Maps using the BrowserAct Google Maps API.

    6.1k GitHub starsUsed in 1 repo~1.4k tokens
    Marketing & SEOAuto-check passed
  • Business Email Extractor

    gmapsscraper/google-maps-agent-skills

    Extract verified business email addresses from Google Maps listings.

    132 GitHub stars~1.1k tokensUpdated 4 mo ago
    Sales & SupportAuto-check passed
  • Google Maps Export

    gmapsscraper/google-maps-agent-skills

    Export Google Maps business data to CSV, JSON, or CRM format (HubSpot, Pipedrive, Salesforce).

    132 GitHub stars~1.2k tokensUpdated 4 mo ago
    Sales & SupportAuto-check passed

More from growthenginenowoslawski/coldoutboundskills

All 49 skills in this repo
  • Email Deliverability Audit

    growthenginenowoslawski/coldoutboundskills

    Diagnostic audit for a running cold email program. An agent skill from growthenginenowoslawski/coldoutboundskills.

    753 GitHub stars~3.4k tokensUpdated 5 days ago
    Auto-check passed
  • Icp Onboarding

    growthenginenowoslawski/coldoutboundskills

    Conversational intake for cold email campaigns. An agent skill from growthenginenowoslawski/coldoutboundskills.

    753 GitHub stars~1.9k tokensUpdated 5 days ago
    Auto-check passed
  • List Builder

    growthenginenowoslawski/coldoutboundskills

    META skill — build the largest possible qualified lead list for any request, end to end.

    753 GitHub stars~4.7k tokensUpdated 5 days ago
    Auto-check: notes
  • Auto Research Public

    growthenginenowoslawski/coldoutboundskills

    Autonomous cold email campaign launcher. An agent skill from growthenginenowoslawski/coldoutboundskills.

    753 GitHub stars~2.9k tokensUpdated 5 days ago
    Auto-check passed
  • Blitz List Builder

    growthenginenowoslawski/coldoutboundskills

    Use the Blitz API to find decision-makers at specific companies when you already have a list of company domains.

    753 GitHub stars~2k tokensUpdated 5 days ago
    Auto-check passed
  • Deliverability Test Public

    growthenginenowoslawski/coldoutboundskills

    Compare reply rates, bounce rates, and positive reply rates broken down by inbox type (SMTP / Gmail / Outlook) for a Smartlead account.

    753 GitHub stars~1k tokensUpdated 5 days ago
    Auto-check passed

Categories

Questions about Google Maps List Builder

What does Google Maps List Builder do?

Scrape Google Maps for local businesses by category and location, output CSV ready for cold email enrichment. Google Maps List Builder is an agent skill from growthenginenowoslawski/coldoutboundskills. Scrape Google Maps for local businesses by category and location, output CSV ready for cold email enrichment.

When should I use Google Maps List Builder?

Google Maps List Builder fits situations like: tasks that involve Cold outreach; tasks that involve Web scraping; tasks that involve CSV and tabular files.

How do I install Google Maps List Builder in Claude Code?

Run `npx skills add growthenginenowoslawski/coldoutboundskills --skill google-maps-list-builder -a claude-code`. Or copy the skill folder (skills/google-maps-list-builder in growthenginenowoslawski/coldoutboundskills) into .claude/skills/google-maps-list-builder in your project. Claude Code loads it when a task matches its description.

How do I install Google Maps List Builder in Codex?

Run `npx skills add growthenginenowoslawski/coldoutboundskills --skill google-maps-list-builder -a codex`. Or copy the skill folder (skills/google-maps-list-builder in growthenginenowoslawski/coldoutboundskills) into .agents/skills/google-maps-list-builder in your project. Codex loads it when a task matches its description.

Can I use Google Maps List Builder in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add growthenginenowoslawski/coldoutboundskills --skill google-maps-list-builder -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/google-maps-list-builder, .gemini/skills/google-maps-list-builder, .github/skills/google-maps-list-builder and .opencode/skills/google-maps-list-builder in your project.

What does Google Maps List Builder need to run?

Going by SKILL.md and its folder, Google Maps List Builder needs the command-line tools its instructions call (npm, npx and fly) and credentials named RAPIDAPI_KEY. Our summary lists: Node.js; A credential in RAPIDAPI_KEY.

Does Google Maps List Builder access the network?

SKILL.md names 1 domain. In commands or code: rapidapi.com; the agent is likely to contact it when it follows the instructions. This is read from the text; nothing was executed.

Is Google Maps List Builder safe to install?

Our automated static check of SKILL.md found notes only (mentions a .env file), nothing it rates as a warning. It is not a guarantee. Review the folder before installing.

What licence does Google Maps List Builder use?

Google Maps List Builder is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Google Maps List Builder use?

About 6.2k tokens (SKILL.md is roughly 25k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Google Maps List Builder?

Skills that share tags, products or a category with Google Maps List Builder: Cold Email Local Business (gmapsscraper/google-maps-agent-skills, 132 stars), Google Maps Reviews Scraper (gmapsscraper/google-maps-agent-skills, 132 stars), Apify Buying Signal Detection (apify/awesome-skills, 266 stars) and Google Maps API Skill (browser-act/skills, 6.1k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Google Maps List Builder?

growthenginenowoslawski (a GitHub user) maintains it in growthenginenowoslawski/coldoutboundskills, which has 753 GitHub stars. The repository holds 49 skills in this directory. The repository was last updated on October 5, 2026.

Source: growthenginenowoslawski/coldoutboundskills on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.